Skip to main content
Tell a dead session from a stalled call, so you never relaunch a browser that was still alive or replay an action that already landed.

Overview

  • A proxy session is one browser, with its tabs and cookies, living in the proxy. chromium.launch() starts one (the MCP server starts one on the first open_page).
  • When the session ends, the browser, tabs and cookies are gone. There is nothing to reconnect to.
  • A brief network drop does not end it. By default the SDK reconnects to the same session. See Resume after a dropped connection.
  • Mock tokens do not expire. Only the session does.
  • Every call carries a deadline, so one hung call never leaves your process waiting forever.

Session limits

A call in flight counts as activity. While the proxy is still working on a call, it keeps resetting the idle timer, and the answer resets it once more. So a single call that runs longer than 15 minutes, such as a fill waiting on the account owner, is not ended by idle_timeout. It can still end at its own deadline (see Proxy side) or at the 60 minute hard limit.
The hard timer is never reset. It fires on its own schedule, not at a safe point between actions, so a call still running at the 60 minute mark can be cut off after it reached the merchant.
A session can outlive one of your turns. The proxy sets no per-turn limit, only these two timers. See Driving a session across turns.

Terminal and non-terminal errors

Four codes mean this session is over. They do not mean “retry this call”. A close with code 4001 arrives as unauthorized instead. Recovery for all four is a fresh chromium.launch(). Before replaying anything, apply the unknown-outcome rule: none of these codes says whether the last action ran. Three codes look like a dead connection and are not terminal: None of the three says whether the call ran. Treat an action as possibly landed, and check before you repeat it. After unanswered or action_timeout the call may still be running, so follow Recover from unanswered. Relaunching after any of them throws away a session that was still there. If you hold a browser across calls, check it before reusing it:

The deadlines

Client side

The SDK gives each call a deadline. Past it, the call rejects with request_timeout and the session closes. requestTimeoutMs(method, params) returns the value for any call. No deadline is longer than 65 minutes, so the session’s 60-minute hard timeout always ends a call first.

The per-action bound

The deadline above answers “is the connection dead?”, so it is generous and it ends the session when it fires. To give up on one hung click sooner and keep the session, set a hard bound per call:
Or set SELF_AGENT_PAY_ACTION_TIMEOUT_MS. Unset means no per-action bound.
  • A call that runs past it rejects with action_timeout. The session stays open, unlike request_timeout.
  • A timeout you pass on the call does not raise it.
  • It applies only when it is shorter than the call’s own deadline.
  • It never applies to browser.close, requestChallengeHandoff, attestedCheckout, selectOption, or any fill, type or pressSequentially. Those can be waiting on the account owner, and the proxy would carry on after you gave up.
The proxy is not told the SDK gave up, so the call may still be running there. Recover exactly as for unanswered.

Proxy side

The proxy keeps its own, shorter deadline on each page call and answers unanswered when Playwright never comes back: 120 seconds, or the call’s own timeout plus 30 seconds, capped at an hour. That deadline is suspended while a call is waiting on the account owner. A few calls keep no proxy deadline: browser.close, attestedCheckout, requestChallengeHandoff, recordOutcome, and any call passed { timeout: 0 }. For those, a stall ends in request_timeout or the hard timer, both terminal.
Keep SELF_AGENT_PAY_REQUEST_TIMEOUT_MS at 150000 or above. The proxy answers a stalled call at 120 seconds with unanswered. A shorter client deadline closes the session before that answer arrives, turning a recoverable stall into a dead session.

Recover from unanswered

The stalled call was abandoned, not cancelled. It keeps running in the proxy with its result dropped, and an action may still land later.
Retry once. The session is intact and the call changed nothing.

Resume after a dropped connection

When the network or a load balancer drops the socket, the SDK redials and takes the same session back. It keeps trying for about 30 seconds, or up to 150 seconds while the proxy is still letting go of the old connection. The browser, its pages, its cookies and its fill state are all still there. Calls you make while it redials wait for it. The proxy holds a dropped session for 60 seconds. A client that has not come back by then finds it closed. Only a call that was in flight across the drop fails, with connection_resumed. Its answer was lost with the socket, so its outcome is unknown:
  • A read, wait or count: retry it.
  • An action: it may have landed. Look at the page on the same browser before repeating it. Do not relaunch.
If the session cannot be resumed (the proxy refused, or the redial ran out of time), the call fails with the terminal connection_closed instead. err.dropReason says why the connection ended: transport_lost, proxy_restart, proxy_error, superseded or resume_refused. Resume is on by default. To end the session on any drop instead, pass chromium.launch({ resume: false }) or set SELF_AGENT_PAY_RESUME=0. A proxy that does not support resume ends the session on a drop either way.

When an outcome is unknown

A missing or connection-level error code means the request got no verdict, not that it never landed. Any close rejects every in-flight call, and the connection can drop after a click or submit reached the merchant. A request_timeout on a card fill does not say whether the fill happened, so a blind retry can pay twice. Check the merchant’s order state, or the transactions and activity feed on your dashboard. The activity feed is best effort, so a missing entry there does not prove the fill did not happen.

browser.outcome() cannot settle it

Its confirmationDetected flag is set only by your own page.recordOutcome() call. The proxy never sets it by observing the merchant.
  • confirmationDetected: true means your agent called recordOutcome(), nothing more. The proxy does not check it against the page. It is only as reliable as the check your agent made first, so confirm against merchant state before you report the order.
  • confirmationDetected: false is inconclusive. An order that succeeded a moment before the failure looks exactly like one that never started.
Once the socket has closed, outcome() is not available at all: it runs over that socket, and a fresh session reports its own empty outcome. The only sound check is durable merchant state.

Driving a session across turns

A browser session lives as long as the process that owns it. Nothing requires launch, act and close to sit in one script. To drive a checkout one step at a time, keep one process running that holds the browser and takes commands, for example from a command file it polls.
pkill -f driver also matches the shell command that launched it. A driver that dies seconds after starting was most likely killed by its own launcher.

Error codes

Every code, and whether retrying is safe.

Confirming outcomes

Session outcome, run report, and what counts as proof.