From 1ca677820c5b2b5a76174649d2c5dad49f42d496 Mon Sep 17 00:00:00 2001 From: Zizheng Tai Date: Thu, 12 Feb 2026 11:43:53 -0800 Subject: [PATCH 1/4] Reduce unnecessary waiting when initializing cloud browsers --- packages/narada/pyproject.toml | 2 +- packages/narada/src/narada/__init__.py | 11 +++--- packages/narada/src/narada/client.py | 46 ++++++++++++++------------ packages/narada/src/narada/utils.py | 10 +++++- uv.lock | 2 +- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/narada/pyproject.toml b/packages/narada/pyproject.toml index dbe3c1f..9546025 100644 --- a/packages/narada/pyproject.toml +++ b/packages/narada/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "narada" -version = "0.1.33a7" +version = "0.1.33a12" description = "Python client SDK for Narada" license = "Apache-2.0" readme = "README.md" diff --git a/packages/narada/src/narada/__init__.py b/packages/narada/src/narada/__init__.py index 220c448..3957d7b 100644 --- a/packages/narada/src/narada/__init__.py +++ b/packages/narada/src/narada/__init__.py @@ -1,8 +1,3 @@ -from narada.client import Narada -from narada.config import BrowserConfig, ProxyConfig -from narada.utils import download_file, render_html -from narada.version import __version__ -from narada.window import CloudBrowserWindow, LocalBrowserWindow, RemoteBrowserWindow from narada_core.errors import ( NaradaError, NaradaExtensionMissingError, @@ -13,6 +8,12 @@ ) from narada_core.models import Agent, File, Response, ResponseContent +from narada.client import Narada +from narada.config import BrowserConfig, ProxyConfig +from narada.utils import download_file, render_html +from narada.version import __version__ +from narada.window import CloudBrowserWindow, LocalBrowserWindow, RemoteBrowserWindow + __all__ = [ "__version__", "Agent", diff --git a/packages/narada/src/narada/client.py b/packages/narada/src/narada/client.py index e08c5f3..173d709 100644 --- a/packages/narada/src/narada/client.py +++ b/packages/narada/src/narada/client.py @@ -33,7 +33,7 @@ from rich.console import Console from narada.config import BrowserConfig, ProxyConfig -from narada.utils import assert_never +from narada.utils import assert_never, assert_not_none from narada.version import __version__ from narada.window import ( CloudBrowserWindow, @@ -222,28 +222,31 @@ async def open_and_initialize_cloud_browser_window( ) except Exception as cleanup_error: logging.warning( - f"Error cleaning up session {session_id}: {cleanup_error}" + "Error cleaning up session %s: %s", session_id, cleanup_error ) # Re-raise the original connection error raise - context = ( - browser.contexts[0] if browser.contexts else await browser.new_context() - ) # Navigate to login URL (provided by backend with custom token) - initialization_page = await context.new_page() + context = browser.contexts[0] + initialization_page = context.pages[0] await initialization_page.goto( login_url, wait_until="domcontentloaded", timeout=60_000 ) - # Wait for sign-in to process to complete. - await asyncio.sleep(15) # TODO: improve it in the future - await initialization_page.reload(wait_until="domcontentloaded", timeout=60_000) - - # Wait for browser window ID - browser_window_id = await self._wait_for_browser_window_id( - initialization_page, config - ) + # Wait for browser window ID. The extension can take a bit to be installed, so we retry a + # few times. + max_attempts = 5 + for attempt in range(max_attempts): + try: + browser_window_id = await self._wait_for_browser_window_id( + initialization_page, config + ) + except NaradaExtensionMissingError: + if attempt == max_attempts - 1: + raise + logging.info("Waiting for Narada extension to be installed...") + await asyncio.sleep(1) # TODO: consider this # Get side panel page @@ -420,7 +423,9 @@ async def _launch_browser( if proxy_requires_auth and not did_initial_navigation: proxy_cdp_session = ( await self._setup_proxy_authentication_browser_level( - browser, config.proxy + browser, + # Not None because `proxy_requires_auth` is True. + assert_not_none(config.proxy), ) ) blank_page = context.pages[0] @@ -476,9 +481,7 @@ async def _wait_for_selector_attached( return None @staticmethod - async def _wait_for_browser_window_id_silently( - page: Page, *, timeout: int = 15_000 - ) -> str: + async def _wait_for_browser_window_id_silently(page: Page, *, timeout: int) -> str: selectors = [ Narada._BROWSER_WINDOW_ID_SELECTOR, Narada._UNSUPPORTED_BROWSER_INDICATOR_SELECTOR, @@ -546,7 +549,7 @@ async def _wait_for_browser_window_id_silently( assert_never() async def _wait_for_browser_window_id_interactively( - self, page: Page, *, per_attempt_timeout: int = 15_000 + self, page: Page, *, per_attempt_timeout: int ) -> str: try: while True: @@ -583,17 +586,18 @@ async def _wait_for_browser_window_id( self, initialization_page: Page, config: BrowserConfig, + timeout: int = 15_000, ) -> str: """Waits for the browser window ID to be available, potentially letting the user respond to recoverable errors interactively. """ if config.interactive: return await self._wait_for_browser_window_id_interactively( - initialization_page + initialization_page, per_attempt_timeout=timeout ) else: return await Narada._wait_for_browser_window_id_silently( - initialization_page + initialization_page, timeout=timeout ) async def _setup_proxy_authentication_browser_level( diff --git a/packages/narada/src/narada/utils.py b/packages/narada/src/narada/utils.py index 68fb807..c4b05eb 100644 --- a/packages/narada/src/narada/utils.py +++ b/packages/narada/src/narada/utils.py @@ -1,13 +1,21 @@ import webbrowser from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Never +from typing import Never, TypeVar + +_T = TypeVar("_T") def assert_never() -> Never: raise AssertionError("Expected code to be unreachable") +def assert_not_none(value: _T | None) -> _T: + if value is None: + raise ValueError("Unexpected None value") + return value + + def download_file(filename: str, content: str | bytes) -> None: """ Downloads a file to the user's Downloads directory. diff --git a/uv.lock b/uv.lock index 6367a9e..ff4556c 100644 --- a/uv.lock +++ b/uv.lock @@ -312,7 +312,7 @@ wheels = [ [[package]] name = "narada" -version = "0.1.33a7" +version = "0.1.33a12" source = { editable = "packages/narada" } dependencies = [ { name = "aiohttp" }, From 6e058dba47255f1c0269c13d3f1dcc3be420d4b0 Mon Sep 17 00:00:00 2001 From: Zizheng Tai Date: Thu, 12 Feb 2026 12:43:17 -0800 Subject: [PATCH 2/4] Refactor exception handling --- packages/narada/src/narada/client.py | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/narada/src/narada/client.py b/packages/narada/src/narada/client.py index 173d709..7491c33 100644 --- a/packages/narada/src/narada/client.py +++ b/packages/narada/src/narada/client.py @@ -164,9 +164,6 @@ async def open_and_initialize_cloud_browser_window( a CDP WebSocket URL. This method connects to it, initializes the extension, and returns a CloudBrowserWindow instance. """ - assert self._playwright is not None - playwright = self._playwright - config = config or BrowserConfig() base_url = os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2") request_body = { @@ -195,12 +192,16 @@ async def open_and_initialize_cloud_browser_window( cdp_websocket_url = response_data["cdp_websocket_url"] session_id = response_data["session_id"] login_url = response_data["login_url"] - cdp_auth_headers = response_data.get("cdp_auth_headers") + cdp_auth_headers = response_data["cdp_auth_headers"] - # Connect to browser via CDP with authentication headers + # Connect to browser via CDP with authentication headers and log the user in. try: - browser = await playwright.chromium.connect_over_cdp( - cdp_websocket_url, headers=cdp_auth_headers + return await self._initialize_cloud_browser_window( + config=config, + cdp_websocket_url=cdp_websocket_url, + session_id=session_id, + login_url=login_url, + cdp_auth_headers=cdp_auth_headers, ) except Exception: # Clean up the session if CDP connection fails @@ -227,6 +228,22 @@ async def open_and_initialize_cloud_browser_window( # Re-raise the original connection error raise + async def _initialize_cloud_browser_window( + self, + *, + config: BrowserConfig, + cdp_websocket_url: str, + session_id: str, + login_url: str, + cdp_auth_headers: dict[str, str], + ) -> CloudBrowserWindow: + assert self._playwright is not None + + # Connect to browser via CDP with authentication headers + browser = await self._playwright.chromium.connect_over_cdp( + cdp_websocket_url, headers=cdp_auth_headers + ) + # Navigate to login URL (provided by backend with custom token) context = browser.contexts[0] initialization_page = context.pages[0] From 61f4dba7dc3a026f6a59c9df9e0025944495718d Mon Sep 17 00:00:00 2001 From: Zizheng Tai Date: Thu, 12 Feb 2026 12:55:19 -0800 Subject: [PATCH 3/4] Bump version --- packages/narada/pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/narada/pyproject.toml b/packages/narada/pyproject.toml index 9546025..71d1894 100644 --- a/packages/narada/pyproject.toml +++ b/packages/narada/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "narada" -version = "0.1.33a12" +version = "0.1.33a13" description = "Python client SDK for Narada" license = "Apache-2.0" readme = "README.md" diff --git a/uv.lock b/uv.lock index ff4556c..4b20d2e 100644 --- a/uv.lock +++ b/uv.lock @@ -312,7 +312,7 @@ wheels = [ [[package]] name = "narada" -version = "0.1.33a12" +version = "0.1.33a13" source = { editable = "packages/narada" } dependencies = [ { name = "aiohttp" }, From d0862b1fd17b92d0d55b70da5356af560fc779ff Mon Sep 17 00:00:00 2001 From: Zizheng Tai Date: Thu, 12 Feb 2026 12:55:47 -0800 Subject: [PATCH 4/4] Clean up logging --- packages/narada/src/narada/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/narada/src/narada/client.py b/packages/narada/src/narada/client.py index 7491c33..20f9fff 100644 --- a/packages/narada/src/narada/client.py +++ b/packages/narada/src/narada/client.py @@ -215,11 +215,14 @@ async def open_and_initialize_cloud_browser_window( ) as resp: if resp.ok: logging.info( - f"Cleaned up session {session_id} after CDP connection failure" + "Cleaned up session %s after CDP connection failure", + session_id, ) else: logging.warning( - f"Failed to cleanup session {session_id}: {resp.status}" + "Failed to cleanup session %s: %s", + session_id, + resp.status, ) except Exception as cleanup_error: logging.warning(