Skip to content

Commit ee9b780

Browse files
committed
always detatch cdp
1 parent 76d7174 commit ee9b780

File tree

2 files changed

+53
-12
lines changed

2 files changed

+53
-12
lines changed

src/stagehand/_custom/session.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class _PlaywrightCDPSession(Protocol):
3939
def send(self, method: str, params: Any = ...) -> Any: # noqa: ANN401
4040
...
4141

42+
def detach(self) -> Any: # noqa: ANN401
43+
...
44+
4245

4346
class _PlaywrightContext(Protocol):
4447
def new_cdp_session(self, page: Any) -> Any: # noqa: ANN401
@@ -71,11 +74,21 @@ def _extract_frame_id_from_playwright_page(page: Any) -> str:
7174
raise StagehandError("Playwright CDP session missing .send(...) method")
7275

7376
pw_cdp = cast(_PlaywrightCDPSession, cdp)
74-
result = pw_cdp.send("Page.getFrameTree")
75-
if inspect.isawaitable(result):
76-
raise StagehandError(
77-
"Expected a synchronous Playwright Page, but received an async CDP session; use AsyncSession methods"
78-
)
77+
try:
78+
result = pw_cdp.send("Page.getFrameTree")
79+
if inspect.isawaitable(result):
80+
raise StagehandError(
81+
"Expected a synchronous Playwright Page, but received an async CDP session; use AsyncSession methods"
82+
)
83+
finally:
84+
detach = getattr(cdp, "detach", None)
85+
if callable(detach):
86+
try:
87+
detach_result = detach()
88+
if inspect.isawaitable(detach_result):
89+
logger.warning("Playwright sync CDP detach() returned an awaitable; session may remain open")
90+
except Exception: # noqa: BLE001
91+
logger.debug("Failed to detach Playwright CDP session", exc_info=True)
7992

8093
try:
8194
return cast(str, result["frameTree"]["frame"]["id"])
@@ -107,9 +120,19 @@ async def _extract_frame_id_from_playwright_page_async(page: Any) -> str:
107120
raise StagehandError("Playwright CDP session missing .send(...) method")
108121

109122
pw_cdp = cast(_PlaywrightCDPSession, cdp)
110-
result = pw_cdp.send("Page.getFrameTree")
111-
if inspect.isawaitable(result):
112-
result = await result
123+
try:
124+
result = pw_cdp.send("Page.getFrameTree")
125+
if inspect.isawaitable(result):
126+
result = await result
127+
finally:
128+
detach = getattr(cdp, "detach", None)
129+
if callable(detach):
130+
try:
131+
detach_result = detach()
132+
if inspect.isawaitable(detach_result):
133+
await detach_result
134+
except Exception: # noqa: BLE001
135+
logger.debug("Failed to detach Playwright CDP session", exc_info=True)
113136

114137
try:
115138
return cast(str, result["frameTree"]["frame"]["id"])

tests/test_session_page_param.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,24 @@
1919
class _SyncCDP:
2020
def __init__(self, frame_id: str) -> None:
2121
self._frame_id = frame_id
22+
self.detached = False
2223

2324
def send(self, method: str) -> dict[str, Any]:
2425
assert method == "Page.getFrameTree"
2526
return {"frameTree": {"frame": {"id": self._frame_id}}}
2627

28+
def detach(self) -> None:
29+
self.detached = True
30+
2731

2832
class _SyncContext:
2933
def __init__(self, frame_id: str) -> None:
3034
self._frame_id = frame_id
35+
self.last_cdp: _SyncCDP | None = None
3136

3237
def new_cdp_session(self, _page: Any) -> _SyncCDP:
33-
return _SyncCDP(self._frame_id)
38+
self.last_cdp = _SyncCDP(self._frame_id)
39+
return self.last_cdp
3440

3541

3642
class _SyncPage:
@@ -41,18 +47,24 @@ def __init__(self, frame_id: str) -> None:
4147
class _AsyncCDP:
4248
def __init__(self, frame_id: str) -> None:
4349
self._frame_id = frame_id
50+
self.detached = False
4451

4552
async def send(self, method: str) -> dict[str, Any]:
4653
assert method == "Page.getFrameTree"
4754
return {"frameTree": {"frame": {"id": self._frame_id}}}
4855

56+
async def detach(self) -> None:
57+
self.detached = True
58+
4959

5060
class _AsyncContext:
5161
def __init__(self, frame_id: str) -> None:
5262
self._frame_id = frame_id
63+
self.last_cdp: _AsyncCDP | None = None
5364

5465
async def new_cdp_session(self, _page: Any) -> _AsyncCDP:
55-
return _AsyncCDP(self._frame_id)
66+
self.last_cdp = _AsyncCDP(self._frame_id)
67+
return self.last_cdp
5668

5769

5870
class _AsyncPage:
@@ -64,6 +76,7 @@ def __init__(self, frame_id: str) -> None:
6476
def test_session_act_injects_frame_id_from_page(respx_mock: MockRouter, client: Stagehand) -> None:
6577
session_id = "00000000-0000-0000-0000-000000000000"
6678
frame_id = "frame-123"
79+
page = _SyncPage(frame_id)
6780

6881
respx_mock.post("/v1/sessions/start").mock(
6982
return_value=httpx.Response(
@@ -80,9 +93,11 @@ def test_session_act_injects_frame_id_from_page(respx_mock: MockRouter, client:
8093
)
8194

8295
session = client.sessions.start(model_name="openai/gpt-5-nano")
83-
session.act(input="click something", page=_SyncPage(frame_id))
96+
session.act(input="click something", page=page)
8497

8598
assert act_route.called is True
99+
assert page.context.last_cdp is not None
100+
assert page.context.last_cdp.detached is True
86101
first_call = cast(Call, act_route.calls[0])
87102
request_body = json.loads(first_call.request.content)
88103
assert request_body["frameId"] == frame_id
@@ -129,6 +144,7 @@ async def test_async_session_act_injects_frame_id_from_page(
129144
) -> None:
130145
session_id = "00000000-0000-0000-0000-000000000000"
131146
frame_id = "frame-async-456"
147+
page = _AsyncPage(frame_id)
132148

133149
respx_mock.post("/v1/sessions/start").mock(
134150
return_value=httpx.Response(
@@ -145,9 +161,11 @@ async def test_async_session_act_injects_frame_id_from_page(
145161
)
146162

147163
session = await async_client.sessions.start(model_name="openai/gpt-5-nano")
148-
await session.act(input="click something", page=_AsyncPage(frame_id))
164+
await session.act(input="click something", page=page)
149165

150166
assert act_route.called is True
167+
assert page.context.last_cdp is not None
168+
assert page.context.last_cdp.detached is True
151169
first_call = cast(Call, act_route.calls[0])
152170
request_body = json.loads(first_call.request.content)
153171
assert request_body["frameId"] == frame_id

0 commit comments

Comments
 (0)