Repository navigation
feat: add browser.disconnect() to the TypeScript and Python SDKs - #3305
Conversation
disconnect() closes Stagehand's connection to a browser and leaves the browser running, whichever way it was obtained: it never sends Browser.close, never closes a launched process or profile, and never releases a Browserbase session. Like close() it is terminal, and later close() or disconnect() calls do nothing. Go is not included.
🦋 Changeset detectedLatest commit: 0406aed The changes in this PR will be included in the next version bump. This PR includes changesets to release 22 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
All reported issues were addressed across 9 files
Architecture diagram
sequenceDiagram
participant User as SDK Caller
participant Browser as StagehandBrowser Handle
participant Internals as Browser Handle Internals
participant CDP as CDP Client
participant BrowserProc as Browser Process (Chrome)
participant BBSession as Browserbase Session
Note over User,BBSession: Browser disconnect flow - detach without closing
User->>Browser: disconnect()
Browser->>Internals: Require internals
Internals->>Internals: Check terminalPromise
alt First disconnect call
Internals->>Internals: Store disconnect callback as terminalPromise
Internals->>CDP: close() (WebSocket connection)
CDP-->>Internals: Connection closed
Internals-->>Browser: Mark as closed
Browser-->>User: Promise resolves
else Subsequent disconnect or close calls
Internals->>Internals: Return existing terminalPromise (no-op)
Internals-->>Browser: Resolve immediately
Browser-->>User: No action
end
Note over BrowserProc,BBSession: Browser remains running
BrowserProc->>BrowserProc: Stays alive (no Browser.close command)
BBSession->>BBSession: Session persists (not released)
User->>User: Browser handle is terminal - cannot re-attach
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
The handle is now a small state machine (open -> disconnected -> closed, or open -> closed). close() after disconnect() releases what Stagehand launched (the local Chrome process and profile, or the Browserbase session) without the connection, and does nothing for connected browsers. Bounds the real Chrome tests at 60 s.
There was a problem hiding this comment.
All reported issues were addressed across 8 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| * session) and does nothing for a browser you connected to; repeated | ||
| * disconnect() calls, or disconnect() after close(), do nothing. | ||
| */ | ||
| disconnect(): Promise<void>; |
There was a problem hiding this comment.
if browserbase session is keepAlive: false (which is the default btw), won't disconnect close the session?
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…rowser.close before terminating launched Chrome
There was a problem hiding this comment.
All reported issues were addressed across 11 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…ailed release after a lost connection
…d; retry a failed Python source close
…ehand; pi closes a lost browser
There was a problem hiding this comment.
All reported issues were addressed across 7 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…feat/browser-disconnect
# why if the sdk process dies without `disconnect()` / `close()` (crash, `SIGKILL`, dropped connection), nothing tells the browser: a local chrome stagehand launched keeps running, and the extension stays initialized, so reconnecting fails with "A Stagehand instance is already initialized". builds on #3305 & adopts the mechanism from `service-watchdog` # what changed - on connect the sdk creates a hidden target on its own cdp connection; chrome destroys it when that connection ends. the extension watches `Target.targetDestroyed` over its own connection, with no heartbeat or timers - on loss: a local browser stagehand launched without `keepAlive` gets `Browser.close`; anything else keeps running, and the extension releases the instance & closes its own connection, so browserbase applies its `keepAlive` policy and a new client can attach - `disconnect()` detaches (disarms the watchdog) before closing; if the detach isn't acknowledged, the socket & handle stay open, since closing would read as a loss - registration checks the marker really is hidden (`type: "other"`); with an older extension or a chrome without hidden targets the sdk warns once & continues without the watchdog - one client per browser: a second client gets "A Stagehand client is already connected" - ts & python; docs & changeset # test plan - extension unit tests: loss/terminate/detach ordering, close of the extension's connection on loss, registration races - sdk unit tests in both sdks: loss policy per provider/origin/keepAlive, unacknowledged detach keeps the handle usable - real chrome (ts & python): `SIGKILL`/`SIGINT` matrix (owned keepAlive=false is shut down, keepAlive=true & externally launched chrome survive and re-attach), disconnect → reconnect → create - live browserbase: hidden target works through the proxy; after `SIGKILL` of the host, a `keepAlive: false` session is `COMPLETED` within 5s, and a `keepAlive: true` session stays `RUNNING` & accepts `connect()` + `Stagehand.create()` --------- Co-authored-by: Sean McGuire <seanmcguire1@outlook.com>
why
there's no public way to detach from a browser without closing it.
browser.close()quits a browser attached withlocalBrowser.connect(), &stagehand.close()keeps the connection open, so callers attached to a long-lived browser (e.g. one holding signed-in profiles) can't cleanly let gowhat changed
browser.disconnect()(ts) /await browser.disconnect()(python): closes an attached stagehand instance (incl. the extension's loopback cdp connection), then stagehand's connection. a laterconnect()+Stagehand.create()on the same browser works; before, it failed with "A Stagehand instance is already initialized". stagehand never sendsBrowser.close, never closes a launched process or profile, & never releases a browserbase sessionkeepAlivedecides whether the session survives: withkeepAlive: false(the default) browserbase ends the session when any cdp connection closes, includingdisconnect(). onlykeepAlive: truesessions keep runningdisconnect():close()then throws ("connect() again to close it"); reconnect withlocalBrowser.connect({ cdpUrl })/browserbase.connect({ sessionId })& close that handle. repeateddisconnect()/close()calls are idempotentdisconnect(),close()still releases a browserbase session over its api & does nothing for local browsersclose()on a launched local browser sendsBrowser.closebefore the launcher terminates it (temp profile removed unlesspreserveUserDataDir)cdpUrl(cdp_urlin python) for local browsers, andconnected:closedonly meansclose()/disconnect()was called, so callers stillclose()a handle whose connection droppeddisconnect()waits at most 10s for the attached stagehand instance to close, then lets go anywayclose()on connected browserstest plan
Browser.closebefore termination,cdpUrl, & re-attachclose()is used instead); & connect → create → disconnect (nostagehand.close()) → connect → create → navigate (fails on the previous head)pnpm check,pnpm test:unit, sdk-ts unit tests, python tests, ruff, ty, docs tests, sdk parity rules