refactor: reorganize OpenClaw services and enhance session management

This commit is contained in:
Abhimanyu Saharan
2026-02-10 14:50:27 +05:30
parent 6f070df74b
commit 82425edd69
24 changed files with 4454 additions and 3380 deletions

View File

@@ -0,0 +1,7 @@
"""OpenClaw lifecycle services package."""
from .constants import * # noqa: F401,F403
from .exceptions import * # noqa: F401,F403
from .provisioning import * # noqa: F401,F403
from .services import * # noqa: F401,F403
from .shared import * # noqa: F401,F403

View File

@@ -0,0 +1,120 @@
"""Shared constants for lifecycle orchestration services."""
from __future__ import annotations
import random
import re
from datetime import timedelta
from typing import Any
_GATEWAY_AGENT_PREFIX = "agent:gateway-"
_GATEWAY_AGENT_SUFFIX = ":main"
_GATEWAY_OPENCLAW_AGENT_PREFIX = "mc-gateway-"
DEFAULT_HEARTBEAT_CONFIG: dict[str, Any] = {
"every": "10m",
"target": "none",
"includeReasoning": False,
}
OFFLINE_AFTER = timedelta(minutes=10)
AGENT_SESSION_PREFIX = "agent"
DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY: dict[str, bool] = {
# Suppress routine HEARTBEAT_OK delivery by default.
"showOk": False,
"showAlerts": True,
"useIndicator": True,
}
DEFAULT_IDENTITY_PROFILE = {
"role": "Generalist",
"communication_style": "direct, concise, practical",
"emoji": ":gear:",
}
IDENTITY_PROFILE_FIELDS = {
"role": "identity_role",
"communication_style": "identity_communication_style",
"emoji": "identity_emoji",
}
EXTRA_IDENTITY_PROFILE_FIELDS = {
"autonomy_level": "identity_autonomy_level",
"verbosity": "identity_verbosity",
"output_format": "identity_output_format",
"update_cadence": "identity_update_cadence",
# Per-agent charter (optional).
# Used to give agents a "purpose in life" and a distinct vibe.
"purpose": "identity_purpose",
"personality": "identity_personality",
"custom_instructions": "identity_custom_instructions",
}
DEFAULT_GATEWAY_FILES = frozenset(
{
"AGENTS.md",
"SOUL.md",
"TASK_SOUL.md",
"SELF.md",
"AUTONOMY.md",
"TOOLS.md",
"IDENTITY.md",
"USER.md",
"HEARTBEAT.md",
"BOOT.md",
"BOOTSTRAP.md",
"MEMORY.md",
},
)
# These files are intended to evolve within the agent workspace.
# Provision them if missing, but avoid overwriting existing content during updates.
#
# Examples:
# - SELF.md: evolving identity/preferences
# - USER.md: human-provided context + lead intake notes
# - MEMORY.md: curated long-term memory (consolidated)
PRESERVE_AGENT_EDITABLE_FILES = frozenset({"SELF.md", "USER.md", "MEMORY.md", "TASK_SOUL.md"})
HEARTBEAT_LEAD_TEMPLATE = "HEARTBEAT_LEAD.md"
HEARTBEAT_AGENT_TEMPLATE = "HEARTBEAT_AGENT.md"
SESSION_KEY_PARTS_MIN = 2
_SESSION_KEY_PARTS_MIN = SESSION_KEY_PARTS_MIN
MAIN_TEMPLATE_MAP = {
"AGENTS.md": "MAIN_AGENTS.md",
"HEARTBEAT.md": "MAIN_HEARTBEAT.md",
"USER.md": "MAIN_USER.md",
"BOOT.md": "MAIN_BOOT.md",
"TOOLS.md": "MAIN_TOOLS.md",
}
_TOOLS_KV_RE = re.compile(r"^(?P<key>[A-Z0-9_]+)=(?P<value>.*)$")
_NON_TRANSIENT_GATEWAY_ERROR_MARKERS = ("unsupported file",)
_TRANSIENT_GATEWAY_ERROR_MARKERS = (
"connect call failed",
"connection refused",
"errno 111",
"econnrefused",
"did not receive a valid http response",
"no route to host",
"network is unreachable",
"host is down",
"name or service not known",
"received 1012",
"service restart",
"http 503",
"http 502",
"http 504",
"temporar",
"timeout",
"timed out",
"connection closed",
"connection reset",
)
_COORDINATION_GATEWAY_TIMEOUT_S = 45.0
_COORDINATION_GATEWAY_BASE_DELAY_S = 0.5
_COORDINATION_GATEWAY_MAX_DELAY_S = 5.0
_SECURE_RANDOM = random.SystemRandom()

View File

@@ -0,0 +1,90 @@
"""OpenClaw-specific exception definitions and mapping helpers."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from fastapi import HTTPException, status
class GatewayOperation(str, Enum):
"""Typed gateway operations used for consistent HTTP error mapping."""
NUDGE_AGENT = "nudge_agent"
SOUL_READ = "soul_read"
SOUL_WRITE = "soul_write"
ASK_USER_DISPATCH = "ask_user_dispatch"
LEAD_MESSAGE_DISPATCH = "lead_message_dispatch"
LEAD_BROADCAST_DISPATCH = "lead_broadcast_dispatch"
ONBOARDING_START_DISPATCH = "onboarding_start_dispatch"
ONBOARDING_ANSWER_DISPATCH = "onboarding_answer_dispatch"
@dataclass(frozen=True, slots=True)
class GatewayErrorPolicy:
"""HTTP policy for mapping gateway operation failures."""
status_code: int
detail_template: str
_GATEWAY_ERROR_POLICIES: dict[GatewayOperation, GatewayErrorPolicy] = {
GatewayOperation.NUDGE_AGENT: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway nudge failed: {error}",
),
GatewayOperation.SOUL_READ: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway SOUL read failed: {error}",
),
GatewayOperation.SOUL_WRITE: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway SOUL update failed: {error}",
),
GatewayOperation.ASK_USER_DISPATCH: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway ask-user dispatch failed: {error}",
),
GatewayOperation.LEAD_MESSAGE_DISPATCH: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway lead message dispatch failed: {error}",
),
GatewayOperation.LEAD_BROADCAST_DISPATCH: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway lead broadcast dispatch failed: {error}",
),
GatewayOperation.ONBOARDING_START_DISPATCH: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway onboarding start dispatch failed: {error}",
),
GatewayOperation.ONBOARDING_ANSWER_DISPATCH: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway onboarding answer dispatch failed: {error}",
),
}
def map_gateway_error_to_http_exception(
operation: GatewayOperation,
exc: Exception,
) -> HTTPException:
"""Map a gateway failure into a typed HTTP exception."""
policy = _GATEWAY_ERROR_POLICIES[operation]
return HTTPException(
status_code=policy.status_code,
detail=policy.detail_template.format(error=str(exc)),
)
def map_gateway_error_message(
operation: GatewayOperation,
exc: Exception,
) -> str:
"""Map a gateway failure into a stable error message string."""
if isinstance(exc, HTTPException):
detail = exc.detail
if isinstance(detail, str):
return detail
return str(detail)
return map_gateway_error_to_http_exception(operation, exc).detail

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
"""Shared OpenClaw lifecycle primitives."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from uuid import UUID, uuid4
from fastapi import HTTPException, status
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import ensure_session, send_message
from app.models.boards import Board
from app.models.gateways import Gateway
from app.services.openclaw.constants import (
_GATEWAY_AGENT_PREFIX,
_GATEWAY_AGENT_SUFFIX,
_GATEWAY_OPENCLAW_AGENT_PREFIX,
)
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
class GatewayAgentIdentity:
"""Naming and identity rules for Mission Control gateway-main agents."""
@classmethod
def session_key_for_id(cls, gateway_id: UUID) -> str:
return f"{_GATEWAY_AGENT_PREFIX}{gateway_id}{_GATEWAY_AGENT_SUFFIX}"
@classmethod
def session_key(cls, gateway: Gateway) -> str:
return cls.session_key_for_id(gateway.id)
@classmethod
def openclaw_agent_id_for_id(cls, gateway_id: UUID) -> str:
return f"{_GATEWAY_OPENCLAW_AGENT_PREFIX}{gateway_id}"
@classmethod
def openclaw_agent_id(cls, gateway: Gateway) -> str:
return cls.openclaw_agent_id_for_id(gateway.id)
async def optional_gateway_config_for_board(
session: AsyncSession,
board: Board,
) -> GatewayClientConfig | None:
"""Return gateway client config when board has a reachable configured gateway."""
if board.gateway_id is None:
return None
gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
if gateway is None or not gateway.url:
return None
return GatewayClientConfig(url=gateway.url, token=gateway.token)
async def require_gateway_config_for_board(
session: AsyncSession,
board: Board,
) -> tuple[Gateway, GatewayClientConfig]:
"""Resolve board gateway and config, raising 422 when unavailable."""
if board.gateway_id is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board is not attached to a gateway",
)
gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
if gateway is None or not gateway.url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway is not configured for this board",
)
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
async def send_gateway_agent_message(
*,
session_key: str,
config: GatewayClientConfig,
agent_name: str,
message: str,
deliver: bool = False,
) -> None:
"""Ensure session and dispatch a message to an agent session."""
await ensure_session(session_key, config=config, label=agent_name)
await send_message(message, session_key=session_key, config=config, deliver=deliver)
def resolve_trace_id(correlation_id: str | None, *, prefix: str) -> str:
"""Resolve a stable trace id from correlation id or generate a scoped fallback."""
normalized = (correlation_id or "").strip()
if normalized:
return normalized
return f"{prefix}:{uuid4().hex[:12]}"
logger = logging.getLogger(__name__)