Skip to content

feat: add browser.disconnect() to the TypeScript and Python SDKs - #3305

Merged
miguelg719 merged 14 commits into
mainfrom
feat/browser-disconnect
Oct 29, 2026
Merged

miguelg719 merged 14 commits into
mainfrom
feat/browser-disconnect

Conversation

@miguelg719

@miguelg719 miguelg719 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

why

there's no public way to detach from a browser without closing it. browser.close() quits a browser attached with localBrowser.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 go

what changed

  • added browser.disconnect() (ts) / await browser.disconnect() (python): closes an attached stagehand instance (incl. the extension's loopback cdp connection), then stagehand's connection. a later connect() + Stagehand.create() on the same browser works; before, it failed with "A Stagehand instance is already initialized". stagehand never sends Browser.close, never closes a launched process or profile, & never releases a browserbase session
  • on browserbase, keepAlive decides whether the session survives: with keepAlive: false (the default) browserbase ends the session when any cdp connection closes, including disconnect(). only keepAlive: true sessions keep running
  • the handle is finished after disconnect(): close() then throws ("connect() again to close it"); reconnect with localBrowser.connect({ cdpUrl }) / browserbase.connect({ sessionId }) & close that handle. repeated disconnect() / close() calls are idempotent
  • if the connection drops without disconnect(), close() still releases a browserbase session over its api & does nothing for local browsers
  • close() on a launched local browser sends Browser.close before the launcher terminates it (temp profile removed unless preserveUserDataDir)
  • handles expose cdpUrl (cdp_url in python) for local browsers, and connected: closed only means close()/disconnect() was called, so callers still close() a handle whose connection dropped
  • disconnect() waits at most 10s for the attached stagehand instance to close, then lets go anyway
  • docs: a disconnect section with a keepAlive warning & a per-case "to close it later" table, a disconnect column in the behavior-by-environment table split by keepAlive, & a corrected note about close() on connected browsers
  • go is intentionally not included

test plan

  • unit tests in both sdks across local & browserbase, launched & connected, keepAlive on & off: idempotency, close after disconnect (throws, releases nothing), close after a lost connection (browserbase released, local untouched), Browser.close before termination, cdpUrl, & re-attach
  • real chrome tests in both sdks: attach to a separately launched chrome, disconnect, & check it still serves its pages (they fail if close() is used instead); & connect → create → disconnect (no stagehand.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

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.
@miguelg719
miguelg719 requested a review from a team as a code owner October 22, 2026 18:42
@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0406aed

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
Name Type
@browserbasehq/stagehand Minor
@browserbasehq/stagehand-python Minor
browse Patch
@browserbasehq/stagehand-examples Patch
@browserbasehq/stagehand-integrations-claude-cua-sdk Patch
@browserbasehq/stagehand-integrations Patch
@browserbasehq/eve Patch
@browserbasehq/stagehand-integrations-gemini-cua-sdk Patch
@browserbasehq/stagehand-integrations-example-pi-facade Patch
@browserbasehq/stagehand-integrations-claude-agent-sdk Patch
@browserbasehq/stagehand-integrations-example-claude-code-facade Patch
@browserbasehq/stagehand-integrations-codex-sdk Patch
@browserbasehq/stagehand-integrations-example-codex-facade Patch
@browserbasehq/stagehand-integrations-cursor-sdk Patch
@browserbasehq/stagehand-integrations-example-cursor-facade Patch
@browserbasehq/stagehand-integrations-deepagents-sdk Patch
@browserbasehq/stagehand-integrations-eve-sdk Patch
@browserbasehq/stagehand-integrations-fx-sdk Patch
@browserbasehq/stagehand-integrations-mastra-sdk Patch
@browserbasehq/stagehand-integrations-example-mastra-facade Patch
@browserbasehq/stagehand-integrations-pi-sdk Patch
@browserbasehq/stagehand-integrations-example-vercel-ai-facade Patch

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

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
Loading

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/sdk-ts/tests/browser-runtime/browserDisconnect.test.ts
Comment thread packages/sdk-ts/src/browser/index.ts Outdated
Comment thread packages/sdk-python/src/stagehand/browser.py
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread packages/docs/v4/configuration/browser.mdx Outdated
* session) and does nothing for a browser you connected to; repeated
* disconnect() calls, or disconnect() after close(), do nothing.
*/
disconnect(): Promise<void>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if browserbase session is keepAlive: false (which is the default btw), won't disconnect close the session?

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread packages/docs/v4/configuration/browser.mdx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread packages/sdk-python/tests/test_browser.py
Comment thread packages/sdk-python/tests/test_browser_disconnect_live.py
…rowser.close before terminating launched Chrome

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread packages/sdk-ts/src/browser/factories.ts
Comment thread packages/sdk-python/src/stagehand/browser.py
Comment thread packages/docs/v4/configuration/browser.mdx Outdated
Comment thread packages/sdk-ts/src/browser/index.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread packages/sdk-ts/tests/browser/factories.test.ts Outdated
Comment thread packages/sdk-python/src/stagehand/browser.py Outdated
@miguelg719
miguelg719 merged commit aeb1ba0 into main Oct 29, 2026
25 checks passed
@miguelg719
miguelg719 deleted the feat/browser-disconnect branch October 29, 2026 01:15
miguelg719 added a commit that referenced this pull request Oct 29, 2026
# 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>

This branch was successfully deployed

1 active deployment
staging - packages/docs — 0406aedb Deployed Oct 28, 2026 by mintlify[bot]
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.

2 participants