diff --git a/backend/app/integrations/notify.py b/backend/app/integrations/notify.py index fc04735d..e56e0e03 100644 --- a/backend/app/integrations/notify.py +++ b/backend/app/integrations/notify.py @@ -185,7 +185,7 @@ def notify_openclaw(session: Session, ctx: NotifyContext) -> None: client.tools_invoke( "sessions_send", {"sessionKey": sk, "message": message}, - timeout_s=3.0, + timeout_s=15.0, ) except Exception: logger.exception("notify_openclaw: sessions_send failed") diff --git a/backend/app/integrations/openclaw.py b/backend/app/integrations/openclaw.py index 780533df..c793f9ae 100644 --- a/backend/app/integrations/openclaw.py +++ b/backend/app/integrations/openclaw.py @@ -2,9 +2,11 @@ from __future__ import annotations import logging import os +import time from typing import Any import requests +from requests.exceptions import ReadTimeout, RequestException logger = logging.getLogger("app.openclaw") @@ -16,6 +18,15 @@ class OpenClawClient: @classmethod def from_env(cls) -> "OpenClawClient | None": + # Ensure .env is loaded into os.environ (pydantic Settings reads env_file but + # does not automatically populate os.environ). + try: + from dotenv import load_dotenv + + load_dotenv(override=False) + except Exception: + pass + url = os.environ.get("OPENCLAW_GATEWAY_URL") token = os.environ.get("OPENCLAW_GATEWAY_TOKEN") if not url or not token: @@ -28,21 +39,49 @@ class OpenClawClient: args: dict[str, Any], *, session_key: str | None = None, - timeout_s: float = 5.0, + timeout_s: float = 10.0, ) -> dict[str, Any]: payload: dict[str, Any] = {"tool": tool, "args": args} logger.info( - "openclaw.tools_invoke", extra={"tool": tool, "has_session_key": bool(session_key)} + "openclaw.tools_invoke", + extra={"tool": tool, "has_session_key": bool(session_key), "timeout_s": timeout_s}, ) if session_key is not None: payload["sessionKey"] = session_key - r = requests.post( - f"{self.base_url}/tools/invoke", - headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}, - json=payload, - timeout=timeout_s, - ) - r.raise_for_status() - logger.info("openclaw.tools_invoke: ok", extra={"tool": tool, "status": r.status_code}) - return r.json() + last_err: Exception | None = None + for attempt in range(2): + try: + r = requests.post( + f"{self.base_url}/tools/invoke", + headers={ + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + }, + json=payload, + # connect timeout, read timeout + timeout=(2.0, timeout_s), + ) + r.raise_for_status() + logger.info( + "openclaw.tools_invoke: ok", + extra={"tool": tool, "status": r.status_code, "attempt": attempt + 1}, + ) + return r.json() + except ReadTimeout as e: + last_err = e + logger.warning( + "openclaw.tools_invoke: timeout", + extra={"tool": tool, "attempt": attempt + 1, "timeout_s": timeout_s}, + ) + time.sleep(0.2 * (attempt + 1)) + except RequestException as e: + last_err = e + logger.warning( + "openclaw.tools_invoke: request error", + extra={"tool": tool, "attempt": attempt + 1, "error": str(e)}, + ) + time.sleep(0.2 * (attempt + 1)) + + assert last_err is not None + raise last_err