Relaunch an exited browser instead of wedging the whole session - #6
lincooln-ai wants to merge 7 commits into
Conversation
A BrowserUse session launches its browser once and keeps that endpoint for its
whole life. If the Chrome process exits (crash, taskkill, OS cleanup of temp
files), the runtime only ever respawns the JS worker, so every later run,
followUp and execute fails with "CDP connection is closed." or "Could not
connect to CDP endpoint." and the session can never recover. reconnect(), the
recovery helper the agent is told to use, re-dials the same dead endpoint.
Browsers this SDK launched now report alive() and can relaunch(), and a session
checks that before a run or a manual cell so an idle browser that died is
replaced instead of poisoning every later call. run() and followUp() surface the
reset as a warning event; execute() relaunches without an event channel.
Caller-owned browsers (Browser.chrome, {cdpUrl}, Browser.cloud) expose no
relaunch: those processes are not ours to restart, so a dead one still fails
loudly.
A browser that dies mid-call keeps failing loudly on purpose: nothing is
replayed automatically, because half-finished browser work is not safe to redo.
Closing a session no longer throws when the browser is already gone: the
SDK-owned tab cleanup skips itself when the endpoint cannot be reached. Temp
profile cleanup also retries, so a profile still held by a dying Chrome child is
not leaked.
"Could not connect to CDP endpoint." gave no way to tell a dead browser from a bad port, a blocked socket or a cloud session that expired, and it forced hosts to match that exact string to react. Connect failures now include the endpoint host and the socket error or close code, while the timeout reports its budget. Only protocol and host are echoed, so devtools paths and cloud session tokens never reach logs or error messages.
There was a problem hiding this comment.
2 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/index.ts">
<violation number="1" location="src/index.ts:219">
P2: When a run is cancelled while a dead browser is being relaunched, the cancellation cannot interrupt `reviveBrowser()`, so `cancel()` and an aborted run signal can wait through the replacement launch before the run observes cancellation. Make browser recovery cancellation-aware and check the signal before starting it.</violation>
</file>
<file name="src/runtime.ts">
<violation number="1" location="src/runtime.ts:247">
P2: If `BrowserUse.close()` runs while a direct `execute()` is waiting for `browser.relaunch()`, this guard discards the replacement after closing the old handle. Coordinate relaunch with session shutdown or close the replacement when the runtime is already closed.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| * reconnects, and targets owned by the previous browser are forgotten with it. | ||
| */ | ||
| async adoptEndpoint(endpoint: string): Promise<void> { | ||
| if (this.closed) return; |
There was a problem hiding this comment.
P2: If BrowserUse.close() runs while a direct execute() is waiting for browser.relaunch(), this guard discards the replacement after closing the old handle. Coordinate relaunch with session shutdown or close the replacement when the runtime is already closed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/runtime.ts, line 247:
<comment>If `BrowserUse.close()` runs while a direct `execute()` is waiting for `browser.relaunch()`, this guard discards the replacement after closing the old handle. Coordinate relaunch with session shutdown or close the replacement when the runtime is already closed.</comment>
<file context>
@@ -239,6 +239,18 @@ export class BrowserRuntime {
+ * reconnects, and targets owned by the previous browser are forgotten with it.
+ */
+ async adoptEndpoint(endpoint: string): Promise<void> {
+ if (this.closed) return;
+ this.config.endpoint = endpoint;
+ this.owned.clear();
</file context>
| this.control = new RunControl((paused) => this.emit({ type: paused ? 'paused' : 'resumed' })); | ||
| this.activeRun = this.performRun(task, options, followUp); | ||
| this.activeRun = (async () => { | ||
| await this.reviveBrowser(); |
There was a problem hiding this comment.
P2: When a run is cancelled while a dead browser is being relaunched, the cancellation cannot interrupt reviveBrowser(), so cancel() and an aborted run signal can wait through the replacement launch before the run observes cancellation. Make browser recovery cancellation-aware and check the signal before starting it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/index.ts, line 219:
<comment>When a run is cancelled while a dead browser is being relaunched, the cancellation cannot interrupt `reviveBrowser()`, so `cancel()` and an aborted run signal can wait through the replacement launch before the run observes cancellation. Make browser recovery cancellation-aware and check the signal before starting it.</comment>
<file context>
@@ -214,7 +215,10 @@ export class BrowserUse {
this.control = new RunControl((paused) => this.emit({ type: paused ? 'paused' : 'resumed' }));
- this.activeRun = this.performRun(task, options, followUp);
+ this.activeRun = (async () => {
+ await this.reviveBrowser();
+ return await this.performRun(task, options, followUp);
+ })();
</file context>
A relaunch that failed threw before performRun started, so its cleanup never ran and every later call reported "This session is busy." Recovery failure now mirrors performRun's cleanup before propagating the error. Recovery also stops racing session teardown: a replacement that finishes launching after close() is closed instead of being leaked, a run that finds the session closed afterwards rejects instead of continuing, and an already aborted signal skips the launch entirely. A launch already in flight still completes: spawning Chrome is not cancellable without plumbing a signal through openBrowser, and the run observes cancellation at its usual checkpoints.
Closing a session connected to the endpoint to close SDK-owned tabs; a transient failure now retries once after a quick refusal, so the cleanup is not dropped just because the browser was slow to answer. A slow failure is not retried, because close() must not spend a second full timeout on an endpoint that is not answering at all, and the cleanup is still dropped rather than thrown when the browser really is gone. A replacement launch is retried once as well, for a profile the dying Chrome still holds, and the first error is kept because it describes the original problem.
The timeout branch now reports the same protocol and host as the socket branch, so both are equally diagnosable, and the test accepts either branch instead of depending on the OS refusing the connection to port 1.
|
Thanks — the P1 was real, and it is now covered by a regression test. Fixed in
Re-verified after these changes:
|
Fixes #5.
What changes
openBrowser()now returns aBrowserHandle; the SDK-launched variant (Browser.chromium) exposesalive()andrelaunch().BrowserUsechecks it before a run or a manual cell and moves the runtime onto the new endpoint, so a Chrome that died while idle no longer wedges every later call.run()/followUp()report the reset as awarningevent;execute()relaunches too but has no event channel.Browser.chrome(...),{ cdpUrl }andBrowser.cloud(...)expose norelaunch— those processes are not ours to restart, so a dead one keeps failing loudly.close()no longer throws when the browser is already gone.BrowserRuntime.close()connects to the endpoint to close SDK-owned tabs; that connect failure is now read as "nothing left to clean up" instead of failing teardown.CDP.connectreports the endpoint host plus the socket error or close code, and the timeout reports its budget. Host only, so devtools paths and cloud session tokens are never echoed.Design note: no relaunch mid-call
Death while idle is cheap to recover: nothing was in flight, and the agent is told the browser was reset. Death during a cell keeps failing loudly on purpose — replaying half-finished browser work automatically is worse than the error. Happy to move that policy (public
relaunchBrowser(), an opt-in flag, …) if you would rather keep the decision in the caller's hands.Verification
execute()after the browser exits: onmain→Error: CDP connection is closed.andsession.close() threw: Could not connect to CDP endpoint.; on this branch →ok, endpoint replaced=trueandsession.close() succeeded.test/browser-relaunch.test.mjsonmainfails withbrowser.alive is not a functionandCould not connect to CDP endpoint; on this branch all three pass. The newcdp.test.mjscase asserts the message names the host and does not echo the devtools path.npm teston this branch: 157 tests, 125 pass, 30 fail, 2 skipped. Onmain: 153 tests, 121 pass, 30 fail, 2 skipped. The failing set is identical (comparing failing test names, not just counts), so the four new tests are the only delta. The 30 failures are pre-existing on Windows: POSIX path assertions inbrowser-options.test.mjs, symlinkEPERMin the evidence adapter, examples ESM resolution.executerecovers within that same call, and killing it while idle leaves the next call working instead of requiring a process restart.Summary by cubic
Fixes issue #5: a session that launched its own browser now relaunches that browser if it exits while idle, so later runs and follow-ups succeed instead of failing with a dead CDP endpoint.
Details
alive()andrelaunch(), and a session relaunches a dead one before a run,followUp, orexecute. A failed relaunch fails the run but leaves the session usable.run/followUpemit awarningevent when a relaunch happens;executesilently relaunches. A replacement launch retries once when the dying profile still holds files, keeping the original error.Browser.chrome,Browser.cloud,{ cdpUrl }) have norelaunch— they keep failing loudly. A browser that dies during a call also fails loudly; nothing is replayed automatically.close()no longer throws when the browser is gone; cleanup retries transient connect failures once, and temp-profile cleanup retries to avoid leaks.Written for commit 784ccc5. Summary will update on new commits.