mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 07:52:20 +02:00
feat(browser): deterministic private_browser lifecycle (Wave 5A)
Own each agent-browser session as a browser tree: the daemon's POSIX session, its runtime files and its Chrome profile. Timeouts, launch failures, bootstrap recovery, cancellation and shutdown clean that tree and verify nothing survives, instead of killing only the daemon and orphaning Chrome. Per-call cleanup no longer sweeps every Chrome under the runtime TMPDIR. Sessionless calls get an ephemeral browser closed before returning. Actions on one session are serialized. Recovery is bounded by one deadline with at most one retry for local HTML open, and the retry flag is no longer model-visible. Observations after a failed navigation are marked stale. read URL navigates and extracts in one batch because agent-browser has no read command. Results carry a browser_lifecycle receipt with stages, timings, ownership and cleanup evidence. Playwright MCP tool calls are bounded by ODYSSEUS_BROWSER_MCP_CALL_TIMEOUT_S and are not retried. research_navigator now passes timeout_ms.
This commit is contained in:
@@ -17,6 +17,18 @@ from src.runtime_paths import get_app_root
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BROWSER_MCP_SERVER_ID = "builtin_browser"
|
||||
|
||||
|
||||
def browser_mcp_call_timeout() -> float:
|
||||
"""Upper bound for one Playwright MCP tool call, in seconds."""
|
||||
|
||||
try:
|
||||
value = float(os.environ.get("ODYSSEUS_BROWSER_MCP_CALL_TIMEOUT_S", "90"))
|
||||
except ValueError:
|
||||
return 90.0
|
||||
return value if value > 0 else 90.0
|
||||
|
||||
def _format_mcp_connection_error(name: str, command: str = "", args: Optional[List[str]] = None, error: Exception = None) -> str:
|
||||
"""Return a user-actionable MCP connection error message."""
|
||||
args = args or []
|
||||
@@ -521,6 +533,24 @@ class McpManager:
|
||||
return {"error": f"MCP server not connected: {server_id}", "exit_code": 1}
|
||||
|
||||
try:
|
||||
if server_id == BROWSER_MCP_SERVER_ID:
|
||||
# The shared Playwright browser must not hold a turn forever.
|
||||
# The call is abandoned, not retried: page state is unknown.
|
||||
limit = browser_mcp_call_timeout()
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self._do_call(session, tool_name, arguments), timeout=limit
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Browser MCP call %s timed out after %ss", tool_name, limit)
|
||||
return {
|
||||
"error": (
|
||||
f"Browser call {tool_name} timed out after {limit:g}s and was "
|
||||
"not retried. The current page state is unknown; navigate "
|
||||
"again before relying on any observation."
|
||||
),
|
||||
"exit_code": 1,
|
||||
}
|
||||
result = await self._do_call(session, tool_name, arguments)
|
||||
except Exception as e:
|
||||
# Auto-reconnect for builtin servers whose subprocess may have died
|
||||
|
||||
Reference in New Issue
Block a user