Skip to content

fix(browser): don't reuse a dead page/context lease in browser run/exec - #324

Open
Kaushik2003 wants to merge 1 commit into
agentrhq:mainfrom
Kaushik2003:fix/314-dead-context-lease
Open

fix(browser): don't reuse a dead page/context lease in browser run/exec#324
Kaushik2003 wants to merge 1 commit into
agentrhq:mainfrom
Kaushik2003:fix/314-dead-context-lease

Conversation

@Kaushik2003

Copy link
Copy Markdown

Description

Related issue: Fixes #314

When you run webcmd browser run against an existing Session, WebCMD tries
to reuse the browser tab (page) it already has open for that session instead
of opening a new one every time — that's normal and good for performance.

The one check it leans on to decide "is this tab still good to use?" is
page.isClosed(). I found that this check can lie. If the browser's
connection dies in a weird way (crashed tab, killed process, connection
dropped from outside Playwright's normal close path), page.isClosed() keeps
saying "still fine!" even though the tab is actually dead. Every real command
sent to it then fails with:

QuickJS promise rejected: Target page, context or browser has been closed

And because nothing told WebCMD "this tab is actually dead, stop using it,"
the next browser run picks the same broken tab again. And the one after
that — it just keeps handing out the same dead lease, which is exactly what
the reporter saw.

I also found a second, sneakier spot with the same root cause: after a script
finishes running, WebCMD takes a snapshot of the page to build a before/after
diff. If that snapshot step hits the same "connection is dead" error,
WebCMD was shrugging it off as a warning and reporting the run as successful
— without ever flagging that the tab needs to be thrown away. So even a run
that did detect the dead connection wasn't fixing anything for next time.

What I changed

I didn't invent anything new here — the codebase already has a working
pattern for this exact problem in a couple of other places
(newPageAttempt, navigatePageAttempt): make a real round trip to check
the connection is alive, and if that fails, throw away the broken session and
start a fresh one. I applied that same pattern to the two spots that didn't
have it yet:

  1. When reusing a tab (getPage() in session-manager.ts): right before
    handing back a "looks fine" tab, I now check the result of the existing
    round-trip call to the browser (it was already being made, I just now act
    on whether it fails). If it fails with the "connection closed" error, I
    throw away the broken session, close the dead tab, and transparently
    acquire a fresh tab instead — the caller never sees the difference, they
    just get a working page.

  2. When the after-run snapshot fails (runner.ts): if that step hits the
    same "connection closed" error, I still report the run as successful (the
    user's script may well have actually worked), but I now also flag the
    session as needing a reset, so the next command on that session gets a
    clean tab instead of the dead one.

Net effect: a dead tab now gets detected and replaced automatically, instead
of being handed out over and over.

Files touched

  • src/browser/runtime/local-cloak/session-manager.ts — the fix for point 1
    above (reused-tab liveness check + fallback to a fresh tab). I also added a
    small helper method other code can call to say "this session's connection
    looks dead, throw it away."
  • src/browser/run/runner.ts — the fix for point 2 (flagging a dead
    connection after a successful run, instead of silently swallowing it).
  • src/browser/run/types.ts — small plumbing: I pulled the "is this a
    connection-closed error" check into one shared place instead of it living
    only inside session-manager.ts, and added the option that lets
    runner.ts signal "this session's connection looks dead" upward.
  • src/browser/runtime/local-cloak/actions.ts — wires the signal from
    runner.ts back to the session manager's new helper, so a run that
    detects a dying connection actually triggers the cleanup.
  • src/browser/run/runner.test.ts and
    src/browser/runtime/local-cloak/session-manager.test.ts — new tests
    (see below).

Tests

  • New test in session-manager.test.ts: simulates the exact bug — a tab
    where isClosed() says "still open" but the underlying connection check
    fails. Confirms WebCMD notices, throws away the broken session, and hands
    back a tab from a fresh session instead of the same broken one.
  • Two new tests in runner.test.ts:
    • Confirms that when the after-run snapshot fails with a "connection
      closed" error, the run still succeeds but the "this connection is
      dying" signal fires.
    • Confirms that an unrelated snapshot failure does not trigger that
      signal — I only want to reset the session for the specific
      "connection is actually dead" case, not for every hiccup.
  • Ran the full existing suite to make sure nothing else broke.
  • npm run typecheck and npm run build — both clean.

What this does not do (on purpose)

This is intentionally a small, focused fix. It does not:

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🌐 New site adapter
  • 📝 Documentation
  • ♻️ Refactor
  • 🔧 CI / build / tooling

Screenshots / Output

$ npm run typecheck
> tsc --noEmit
(clean)

$ npm run build
(clean)

$ npm test
 Test Files  442 passed (442)
      Tests  5647 passed | 2 skipped (5649)

page.isClosed() can keep reporting false even after the underlying CDP
connection has actually died (crashed renderer, killed process, dropped
pipe), so getPage() was handing the same broken lease back out to every
browser run/exec command on a Session, matching the repro in agentrhq#314.

- getPage()'s reuse fast path now wraps its existing liveness probe
  (assertOwnedWindow, which already makes a real CDP round trip) in a
  try/catch. On a closed-context error it invalidates the Profile runtime
  and falls through to acquire a fresh page instead of returning the stale
  one.
- runBrowserProgram()'s post-run snapshot capture can hit the same
  closed-context error while still reporting ok: true (the script may have
  genuinely succeeded). It now signals that upward via a new onStaleContext
  option so the run action can invalidate the runtime instead of silently
  downgrading it to a warning.
- Added CloakSessionManager.invalidateIfClosedContext() as the public hook
  actions.ts uses to wire that signal through.
- Hoisted isClosedContextError() into run/types.ts as a shared helper
  instead of a private duplicate in session-manager.ts.

Fixes agentrhq#314
@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

This review is advisory and does not block merging.

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.

browser run: recover when an apparently open page belongs to a dead context

1 participant