optexity-browser-use 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,253 @@
1
+ """About:blank watchdog for managing about:blank tabs with DVD screensaver."""
2
+
3
+ from typing import TYPE_CHECKING, ClassVar
4
+
5
+ from bubus import BaseEvent
6
+ from cdp_use.cdp.target import TargetID
7
+ from pydantic import PrivateAttr
8
+
9
+ from browser_use.browser.events import (
10
+ AboutBlankDVDScreensaverShownEvent,
11
+ BrowserStopEvent,
12
+ BrowserStoppedEvent,
13
+ CloseTabEvent,
14
+ NavigateToUrlEvent,
15
+ TabClosedEvent,
16
+ TabCreatedEvent,
17
+ )
18
+ from browser_use.browser.watchdog_base import BaseWatchdog
19
+
20
+ if TYPE_CHECKING:
21
+ pass
22
+
23
+
24
+ class AboutBlankWatchdog(BaseWatchdog):
25
+ """Ensures there's always exactly one about:blank tab with DVD screensaver."""
26
+
27
+ # Event contracts
28
+ LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [
29
+ BrowserStopEvent,
30
+ BrowserStoppedEvent,
31
+ TabCreatedEvent,
32
+ TabClosedEvent,
33
+ ]
34
+ EMITS: ClassVar[list[type[BaseEvent]]] = [
35
+ NavigateToUrlEvent,
36
+ CloseTabEvent,
37
+ AboutBlankDVDScreensaverShownEvent,
38
+ ]
39
+
40
+ _stopping: bool = PrivateAttr(default=False)
41
+
42
+ async def on_BrowserStopEvent(self, event: BrowserStopEvent) -> None:
43
+ """Handle browser stop request - stop creating new tabs."""
44
+ # logger.info('[AboutBlankWatchdog] Browser stop requested, stopping tab creation')
45
+ self._stopping = True
46
+
47
+ async def on_BrowserStoppedEvent(self, event: BrowserStoppedEvent) -> None:
48
+ """Handle browser stopped event."""
49
+ # logger.info('[AboutBlankWatchdog] Browser stopped')
50
+ self._stopping = True
51
+
52
+ async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
53
+ """Check tabs when a new tab is created."""
54
+ # logger.debug(f'[AboutBlankWatchdog] ➕ New tab created: {event.url}')
55
+
56
+ # If an about:blank tab was created, show DVD screensaver on all about:blank tabs
57
+ if event.url == 'about:blank':
58
+ await self._show_dvd_screensaver_on_about_blank_tabs()
59
+
60
+ async def on_TabClosedEvent(self, event: TabClosedEvent) -> None:
61
+ """Check tabs when a tab is closed and proactively create about:blank if needed."""
62
+ # logger.debug('[AboutBlankWatchdog] Tab closing, checking if we need to create about:blank tab')
63
+
64
+ # Don't create new tabs if browser is shutting down
65
+ if self._stopping:
66
+ # logger.debug('[AboutBlankWatchdog] Browser is stopping, not creating new tabs')
67
+ return
68
+
69
+ # Check if we're about to close the last tab (event happens BEFORE tab closes)
70
+ # Use _cdp_get_all_pages for quick check without fetching titles
71
+ page_targets = await self.browser_session._cdp_get_all_pages()
72
+ if len(page_targets) <= 1:
73
+ self.logger.debug(
74
+ '[AboutBlankWatchdog] Last tab closing, creating new about:blank tab to avoid closing entire browser'
75
+ )
76
+ # Create the animation tab since no tabs should remain
77
+ navigate_event = self.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
78
+ await navigate_event
79
+ # Show DVD screensaver on the new tab
80
+ await self._show_dvd_screensaver_on_about_blank_tabs()
81
+ else:
82
+ # Multiple tabs exist, check after close
83
+ await self._check_and_ensure_about_blank_tab()
84
+
85
+ async def attach_to_target(self, target_id: TargetID) -> None:
86
+ """AboutBlankWatchdog doesn't monitor individual targets."""
87
+ pass
88
+
89
+ async def _check_and_ensure_about_blank_tab(self) -> None:
90
+ """Check current tabs and ensure exactly one about:blank tab with animation exists."""
91
+ try:
92
+ # For quick checks, just get page targets without titles to reduce noise
93
+ page_targets = await self.browser_session._cdp_get_all_pages()
94
+
95
+ # If no tabs exist at all, create one to keep browser alive
96
+ if len(page_targets) == 0:
97
+ # Only create a new tab if there are no tabs at all
98
+ self.logger.debug('[AboutBlankWatchdog] No tabs exist, creating new about:blank DVD screensaver tab')
99
+ navigate_event = self.event_bus.dispatch(NavigateToUrlEvent(url='about:blank', new_tab=True))
100
+ await navigate_event
101
+ # Show DVD screensaver on the new tab
102
+ await self._show_dvd_screensaver_on_about_blank_tabs()
103
+ # Otherwise there are tabs, don't create new ones to avoid interfering
104
+
105
+ except Exception as e:
106
+ self.logger.error(f'[AboutBlankWatchdog] Error ensuring about:blank tab: {e}')
107
+
108
+ async def _show_dvd_screensaver_on_about_blank_tabs(self) -> None:
109
+ """Show DVD screensaver on all about:blank pages only."""
110
+ try:
111
+ # Get just the page targets without expensive title fetching
112
+ page_targets = await self.browser_session._cdp_get_all_pages()
113
+ browser_session_label = str(self.browser_session.id)[-4:]
114
+
115
+ for page_target in page_targets:
116
+ target_id = page_target['targetId']
117
+ url = page_target['url']
118
+
119
+ # Only target about:blank pages specifically
120
+ if url == 'about:blank':
121
+ await self._show_dvd_screensaver_loading_animation_cdp(target_id, browser_session_label)
122
+
123
+ except Exception as e:
124
+ self.logger.error(f'[AboutBlankWatchdog] Error showing DVD screensaver: {e}')
125
+
126
+ async def _show_dvd_screensaver_loading_animation_cdp(self, target_id: TargetID, browser_session_label: str) -> None:
127
+ """
128
+ Injects a DVD screensaver-style bouncing logo loading animation overlay into the target using CDP.
129
+ This is used to visually indicate that the browser is setting up or waiting.
130
+ """
131
+ try:
132
+ # Create temporary session for this target without switching focus
133
+ temp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)
134
+
135
+ # Inject the DVD screensaver script (from main branch with idempotency added)
136
+ script = f"""
137
+ (function(browser_session_label) {{
138
+ // Idempotency check
139
+ if (window.__dvdAnimationRunning) {{
140
+ return; // Already running, don't add another
141
+ }}
142
+ window.__dvdAnimationRunning = true;
143
+
144
+ // Ensure document.body exists before proceeding
145
+ if (!document.body) {{
146
+ // Try again after DOM is ready
147
+ window.__dvdAnimationRunning = false; // Reset flag to retry
148
+ if (document.readyState === 'loading') {{
149
+ document.addEventListener('DOMContentLoaded', () => arguments.callee(browser_session_label));
150
+ }}
151
+ return;
152
+ }}
153
+
154
+ const animated_title = `Starting agent ${{browser_session_label}}...`;
155
+ if (document.title === animated_title) {{
156
+ return; // already run on this tab, dont run again
157
+ }}
158
+ document.title = animated_title;
159
+
160
+ // Create the main overlay
161
+ const loadingOverlay = document.createElement('div');
162
+ loadingOverlay.id = 'pretty-loading-animation';
163
+ loadingOverlay.style.position = 'fixed';
164
+ loadingOverlay.style.top = '0';
165
+ loadingOverlay.style.left = '0';
166
+ loadingOverlay.style.width = '100vw';
167
+ loadingOverlay.style.height = '100vh';
168
+ loadingOverlay.style.background = '#000';
169
+ loadingOverlay.style.zIndex = '99999';
170
+ loadingOverlay.style.overflow = 'hidden';
171
+
172
+ // Create the image element
173
+ const img = document.createElement('img');
174
+ img.src = 'https://cf.browser-use.com/logo.svg';
175
+ img.alt = 'Browser-Use';
176
+ img.style.width = '200px';
177
+ img.style.height = 'auto';
178
+ img.style.position = 'absolute';
179
+ img.style.left = '0px';
180
+ img.style.top = '0px';
181
+ img.style.zIndex = '2';
182
+ img.style.opacity = '0.8';
183
+
184
+ loadingOverlay.appendChild(img);
185
+ document.body.appendChild(loadingOverlay);
186
+
187
+ // DVD screensaver bounce logic
188
+ let x = Math.random() * (window.innerWidth - 300);
189
+ let y = Math.random() * (window.innerHeight - 300);
190
+ let dx = 1.2 + Math.random() * 0.4; // px per frame
191
+ let dy = 1.2 + Math.random() * 0.4;
192
+ // Randomize direction
193
+ if (Math.random() > 0.5) dx = -dx;
194
+ if (Math.random() > 0.5) dy = -dy;
195
+
196
+ function animate() {{
197
+ const imgWidth = img.offsetWidth || 300;
198
+ const imgHeight = img.offsetHeight || 300;
199
+ x += dx;
200
+ y += dy;
201
+
202
+ if (x <= 0) {{
203
+ x = 0;
204
+ dx = Math.abs(dx);
205
+ }} else if (x + imgWidth >= window.innerWidth) {{
206
+ x = window.innerWidth - imgWidth;
207
+ dx = -Math.abs(dx);
208
+ }}
209
+ if (y <= 0) {{
210
+ y = 0;
211
+ dy = Math.abs(dy);
212
+ }} else if (y + imgHeight >= window.innerHeight) {{
213
+ y = window.innerHeight - imgHeight;
214
+ dy = -Math.abs(dy);
215
+ }}
216
+
217
+ img.style.left = `${{x}}px`;
218
+ img.style.top = `${{y}}px`;
219
+
220
+ requestAnimationFrame(animate);
221
+ }}
222
+ animate();
223
+
224
+ // Responsive: update bounds on resize
225
+ window.addEventListener('resize', () => {{
226
+ x = Math.min(x, window.innerWidth - img.offsetWidth);
227
+ y = Math.min(y, window.innerHeight - img.offsetHeight);
228
+ }});
229
+
230
+ // Add a little CSS for smoothness
231
+ const style = document.createElement('style');
232
+ style.textContent = `
233
+ #pretty-loading-animation {{
234
+ /*backdrop-filter: blur(2px) brightness(0.9);*/
235
+ }}
236
+ #pretty-loading-animation img {{
237
+ user-select: none;
238
+ pointer-events: none;
239
+ }}
240
+ `;
241
+ document.head.appendChild(style);
242
+ }})('{browser_session_label}');
243
+ """
244
+
245
+ await temp_session.cdp_client.send.Runtime.evaluate(params={'expression': script}, session_id=temp_session.session_id)
246
+
247
+ # No need to detach - session is cached
248
+
249
+ # Dispatch event
250
+ self.event_bus.dispatch(AboutBlankDVDScreensaverShownEvent(target_id=target_id))
251
+
252
+ except Exception as e:
253
+ self.logger.error(f'[AboutBlankWatchdog] Error injecting DVD screensaver: {e}')
@@ -0,0 +1,335 @@
1
+ """Browser watchdog for monitoring crashes and network timeouts using CDP."""
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import TYPE_CHECKING, ClassVar
6
+
7
+ import psutil
8
+ from bubus import BaseEvent
9
+ from cdp_use.cdp.target import SessionID, TargetID
10
+ from cdp_use.cdp.target.events import TargetCrashedEvent
11
+ from pydantic import Field, PrivateAttr
12
+
13
+ from browser_use.browser.events import (
14
+ BrowserConnectedEvent,
15
+ BrowserErrorEvent,
16
+ BrowserStoppedEvent,
17
+ TabClosedEvent,
18
+ TabCreatedEvent,
19
+ )
20
+ from browser_use.browser.watchdog_base import BaseWatchdog
21
+
22
+ if TYPE_CHECKING:
23
+ pass
24
+
25
+
26
+ class NetworkRequestTracker:
27
+ """Tracks ongoing network requests."""
28
+
29
+ def __init__(self, request_id: str, start_time: float, url: str, method: str, resource_type: str | None = None):
30
+ self.request_id = request_id
31
+ self.start_time = start_time
32
+ self.url = url
33
+ self.method = method
34
+ self.resource_type = resource_type
35
+
36
+
37
+ class CrashWatchdog(BaseWatchdog):
38
+ """Monitors browser health for crashes and network timeouts using CDP."""
39
+
40
+ # Event contracts
41
+ LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [
42
+ BrowserConnectedEvent,
43
+ BrowserStoppedEvent,
44
+ TabCreatedEvent,
45
+ TabClosedEvent,
46
+ ]
47
+ EMITS: ClassVar[list[type[BaseEvent]]] = [BrowserErrorEvent]
48
+
49
+ # Configuration
50
+ network_timeout_seconds: float = Field(default=10.0)
51
+ check_interval_seconds: float = Field(default=5.0) # Reduced frequency to reduce noise
52
+
53
+ # Private state
54
+ _active_requests: dict[str, NetworkRequestTracker] = PrivateAttr(default_factory=dict)
55
+ _monitoring_task: asyncio.Task | None = PrivateAttr(default=None)
56
+ _last_responsive_checks: dict[str, float] = PrivateAttr(default_factory=dict) # target_url -> timestamp
57
+ _cdp_event_tasks: set[asyncio.Task] = PrivateAttr(default_factory=set) # Track CDP event handler tasks
58
+ _targets_with_listeners: set[str] = PrivateAttr(default_factory=set) # Track targets that already have event listeners
59
+
60
+ async def on_BrowserConnectedEvent(self, event: BrowserConnectedEvent) -> None:
61
+ """Start monitoring when browser is connected."""
62
+ # logger.debug('[CrashWatchdog] Browser connected event received, beginning monitoring')
63
+
64
+ asyncio.create_task(self._start_monitoring())
65
+ # logger.debug(f'[CrashWatchdog] Monitoring task started: {self._monitoring_task and not self._monitoring_task.done()}')
66
+
67
+ async def on_BrowserStoppedEvent(self, event: BrowserStoppedEvent) -> None:
68
+ """Stop monitoring when browser stops."""
69
+ # logger.debug('[CrashWatchdog] Browser stopped, ending monitoring')
70
+ await self._stop_monitoring()
71
+
72
+ async def on_TabCreatedEvent(self, event: TabCreatedEvent) -> None:
73
+ """Attach to new tab."""
74
+ assert self.browser_session.agent_focus is not None, 'No current target ID'
75
+ await self.attach_to_target(self.browser_session.agent_focus.target_id)
76
+
77
+ async def on_TabClosedEvent(self, event: TabClosedEvent) -> None:
78
+ """Clean up tracking when tab closes."""
79
+ # Remove target from listener tracking to prevent memory leak
80
+ if event.target_id in self._targets_with_listeners:
81
+ self._targets_with_listeners.discard(event.target_id)
82
+ self.logger.debug(f'[CrashWatchdog] Removed target {event.target_id[:8]}... from monitoring')
83
+
84
+ async def attach_to_target(self, target_id: TargetID) -> None:
85
+ """Set up crash monitoring for a specific target using CDP."""
86
+ try:
87
+ # Check if we already have listeners for this target
88
+ if target_id in self._targets_with_listeners:
89
+ self.logger.debug(f'[CrashWatchdog] Event listeners already exist for target: {target_id[:8]}...')
90
+ return
91
+
92
+ # Create temporary session for monitoring without switching focus
93
+ cdp_session = await self.browser_session.get_or_create_cdp_session(target_id, focus=False)
94
+
95
+ # Register crash event handler
96
+ def on_target_crashed(event: TargetCrashedEvent, session_id: SessionID | None = None):
97
+ # Create and track the task
98
+ task = asyncio.create_task(self._on_target_crash_cdp(target_id))
99
+ self._cdp_event_tasks.add(task)
100
+ # Remove from set when done
101
+ task.add_done_callback(lambda t: self._cdp_event_tasks.discard(t))
102
+
103
+ cdp_session.cdp_client.register.Target.targetCrashed(on_target_crashed)
104
+
105
+ # Track that we've added listeners to this target
106
+ self._targets_with_listeners.add(target_id)
107
+
108
+ # Get target info for logging
109
+ targets = await cdp_session.cdp_client.send.Target.getTargets()
110
+ target_info = next((t for t in targets['targetInfos'] if t['targetId'] == target_id), None)
111
+ if target_info:
112
+ self.logger.debug(f'[CrashWatchdog] Added target to monitoring: {target_info.get("url", "unknown")}')
113
+
114
+ except Exception as e:
115
+ self.logger.warning(f'[CrashWatchdog] Failed to attach to target {target_id}: {e}')
116
+
117
+ async def _on_request_cdp(self, event: dict) -> None:
118
+ """Track new network request from CDP event."""
119
+ request_id = event.get('requestId', '')
120
+ request = event.get('request', {})
121
+
122
+ self._active_requests[request_id] = NetworkRequestTracker(
123
+ request_id=request_id,
124
+ start_time=time.time(),
125
+ url=request.get('url', ''),
126
+ method=request.get('method', ''),
127
+ resource_type=event.get('type'),
128
+ )
129
+ # logger.debug(f'[CrashWatchdog] Tracking request: {request.get("method", "")} {request.get("url", "")[:50]}...')
130
+
131
+ def _on_response_cdp(self, event: dict) -> None:
132
+ """Remove request from tracking on response."""
133
+ request_id = event.get('requestId', '')
134
+ if request_id in self._active_requests:
135
+ elapsed = time.time() - self._active_requests[request_id].start_time
136
+ response = event.get('response', {})
137
+ self.logger.debug(f'[CrashWatchdog] Request completed in {elapsed:.2f}s: {response.get("url", "")[:50]}...')
138
+ # Don't remove yet - wait for loadingFinished
139
+
140
+ def _on_request_failed_cdp(self, event: dict) -> None:
141
+ """Remove request from tracking on failure."""
142
+ request_id = event.get('requestId', '')
143
+ if request_id in self._active_requests:
144
+ elapsed = time.time() - self._active_requests[request_id].start_time
145
+ self.logger.debug(
146
+ f'[CrashWatchdog] Request failed after {elapsed:.2f}s: {self._active_requests[request_id].url[:50]}...'
147
+ )
148
+ del self._active_requests[request_id]
149
+
150
+ def _on_request_finished_cdp(self, event: dict) -> None:
151
+ """Remove request from tracking when loading is finished."""
152
+ request_id = event.get('requestId', '')
153
+ self._active_requests.pop(request_id, None)
154
+
155
+ async def _on_target_crash_cdp(self, target_id: TargetID) -> None:
156
+ """Handle target crash detected via CDP."""
157
+ self.logger.debug(f'[CrashWatchdog] Target crashed: {target_id[:8]}..., waiting for detach event')
158
+
159
+ # Get target info for logging
160
+ cdp_client = self.browser_session.cdp_client
161
+ targets = await cdp_client.send.Target.getTargets()
162
+ target_info = next((t for t in targets['targetInfos'] if t['targetId'] == target_id), None)
163
+
164
+ is_agent_focus = (
165
+ target_info
166
+ and self.browser_session.agent_focus
167
+ and target_info['targetId'] == self.browser_session.agent_focus.target_id
168
+ )
169
+
170
+ if is_agent_focus and target_info:
171
+ self.logger.error(
172
+ f'[CrashWatchdog] 💥 Agent focus tab crashed: {target_info.get("url", "unknown")} '
173
+ f'(SessionManager will auto-recover)'
174
+ )
175
+
176
+ # Emit browser error event
177
+ self.event_bus.dispatch(
178
+ BrowserErrorEvent(
179
+ error_type='TargetCrash',
180
+ message=f'Target crashed: {target_id}',
181
+ details={
182
+ 'url': target_info.get('url') if target_info else None,
183
+ 'target_id': target_id,
184
+ 'was_agent_focus': is_agent_focus,
185
+ },
186
+ )
187
+ )
188
+
189
+ async def _start_monitoring(self) -> None:
190
+ """Start the monitoring loop."""
191
+ assert self.browser_session.cdp_client is not None, 'Root CDP client not initialized - browser may not be connected yet'
192
+
193
+ if self._monitoring_task and not self._monitoring_task.done():
194
+ # logger.info('[CrashWatchdog] Monitoring already running')
195
+ return
196
+
197
+ self._monitoring_task = asyncio.create_task(self._monitoring_loop())
198
+ # logger.debug('[CrashWatchdog] Monitoring loop created and started')
199
+
200
+ async def _stop_monitoring(self) -> None:
201
+ """Stop the monitoring loop and clean up all tracking."""
202
+ if self._monitoring_task and not self._monitoring_task.done():
203
+ self._monitoring_task.cancel()
204
+ try:
205
+ await self._monitoring_task
206
+ except asyncio.CancelledError:
207
+ pass
208
+ self.logger.debug('[CrashWatchdog] Monitoring loop stopped')
209
+
210
+ # Cancel all CDP event handler tasks
211
+ for task in list(self._cdp_event_tasks):
212
+ if not task.done():
213
+ task.cancel()
214
+ # Wait for all tasks to complete cancellation
215
+ if self._cdp_event_tasks:
216
+ await asyncio.gather(*self._cdp_event_tasks, return_exceptions=True)
217
+ self._cdp_event_tasks.clear()
218
+
219
+ # Clear all tracking
220
+ self._active_requests.clear()
221
+ self._targets_with_listeners.clear()
222
+ self._last_responsive_checks.clear()
223
+
224
+ async def _monitoring_loop(self) -> None:
225
+ """Main monitoring loop."""
226
+ await asyncio.sleep(10) # give browser time to start up and load the first page after first LLM call
227
+ while True:
228
+ try:
229
+ await self._check_network_timeouts()
230
+ await self._check_browser_health()
231
+ await asyncio.sleep(self.check_interval_seconds)
232
+ except asyncio.CancelledError:
233
+ break
234
+ except Exception as e:
235
+ self.logger.error(f'[CrashWatchdog] Error in monitoring loop: {e}')
236
+
237
+ async def _check_network_timeouts(self) -> None:
238
+ """Check for network requests exceeding timeout."""
239
+ current_time = time.time()
240
+ timed_out_requests = []
241
+
242
+ # Debug logging
243
+ if self._active_requests:
244
+ self.logger.debug(
245
+ f'[CrashWatchdog] Checking {len(self._active_requests)} active requests for timeouts (threshold: {self.network_timeout_seconds}s)'
246
+ )
247
+
248
+ for request_id, tracker in self._active_requests.items():
249
+ elapsed = current_time - tracker.start_time
250
+ self.logger.debug(
251
+ f'[CrashWatchdog] Request {tracker.url[:30]}... elapsed: {elapsed:.1f}s, timeout: {self.network_timeout_seconds}s'
252
+ )
253
+ if elapsed >= self.network_timeout_seconds:
254
+ timed_out_requests.append((request_id, tracker))
255
+
256
+ # Emit events for timed out requests
257
+ for request_id, tracker in timed_out_requests:
258
+ self.logger.warning(
259
+ f'[CrashWatchdog] Network request timeout after {self.network_timeout_seconds}s: '
260
+ f'{tracker.method} {tracker.url[:100]}...'
261
+ )
262
+
263
+ self.event_bus.dispatch(
264
+ BrowserErrorEvent(
265
+ error_type='NetworkTimeout',
266
+ message=f'Network request timed out after {self.network_timeout_seconds}s',
267
+ details={
268
+ 'url': tracker.url,
269
+ 'method': tracker.method,
270
+ 'resource_type': tracker.resource_type,
271
+ 'elapsed_seconds': current_time - tracker.start_time,
272
+ },
273
+ )
274
+ )
275
+
276
+ # Remove from tracking
277
+ del self._active_requests[request_id]
278
+
279
+ async def _check_browser_health(self) -> None:
280
+ """Check if browser and targets are still responsive."""
281
+
282
+ try:
283
+ self.logger.debug(f'[CrashWatchdog] Checking browser health for target {self.browser_session.agent_focus}')
284
+ cdp_session = await self.browser_session.get_or_create_cdp_session()
285
+
286
+ for target in (await self.browser_session.cdp_client.send.Target.getTargets()).get('targetInfos', []):
287
+ if target.get('type') == 'page':
288
+ cdp_session = await self.browser_session.get_or_create_cdp_session(target_id=target.get('targetId'))
289
+ if self._is_new_tab_page(target.get('url')) and target.get('url') != 'about:blank':
290
+ self.logger.debug(
291
+ f'[CrashWatchdog] Redirecting chrome://new-tab-page/ to about:blank {target.get("url")}'
292
+ )
293
+ await cdp_session.cdp_client.send.Page.navigate(
294
+ params={'url': 'about:blank'}, session_id=cdp_session.session_id
295
+ )
296
+
297
+ # Quick ping to check if session is alive
298
+ self.logger.debug(f'[CrashWatchdog] Attempting to run simple JS test expression in session {cdp_session} 1+1')
299
+ await asyncio.wait_for(
300
+ cdp_session.cdp_client.send.Runtime.evaluate(params={'expression': '1+1'}, session_id=cdp_session.session_id),
301
+ timeout=1.0,
302
+ )
303
+ self.logger.debug(f'[CrashWatchdog] Browser health check passed for target {self.browser_session.agent_focus}')
304
+ except Exception as e:
305
+ self.logger.error(
306
+ f'[CrashWatchdog] ❌ Crashed/unresponsive session detected for target {self.browser_session.agent_focus} '
307
+ f'error: {type(e).__name__}: {e} (Chrome will send detach event, SessionManager will auto-recover)'
308
+ )
309
+
310
+ # Check browser process if we have PID
311
+ if self.browser_session._local_browser_watchdog and (proc := self.browser_session._local_browser_watchdog._subprocess):
312
+ try:
313
+ if proc.status() in (psutil.STATUS_ZOMBIE, psutil.STATUS_DEAD):
314
+ self.logger.error(f'[CrashWatchdog] Browser process {proc.pid} has crashed')
315
+
316
+ # Browser process crashed - SessionManager will clean up via detach events
317
+ # Just dispatch error event and stop monitoring
318
+ self.event_bus.dispatch(
319
+ BrowserErrorEvent(
320
+ error_type='BrowserProcessCrashed',
321
+ message=f'Browser process {proc.pid} has crashed',
322
+ details={'pid': proc.pid, 'status': proc.status()},
323
+ )
324
+ )
325
+
326
+ self.logger.warning('[CrashWatchdog] Browser process dead - stopping health monitoring')
327
+ await self._stop_monitoring()
328
+ return
329
+ except Exception:
330
+ pass # psutil not available or process doesn't exist
331
+
332
+ @staticmethod
333
+ def _is_new_tab_page(url: str) -> bool:
334
+ """Check if URL is a new tab page."""
335
+ return url in ['about:blank', 'chrome://new-tab-page/', 'chrome://newtab/']