From 264200979298d6b8f46d5ae0b32963858f552ed3 Mon Sep 17 00:00:00 2001 From: Alexander Minges Date: Tue, 10 Feb 2026 12:20:55 +0100 Subject: [PATCH] feat(matrix): add configurable graceful sync shutdown --- nanobot/channels/matrix.py | 21 ++++++++++++++++++++- nanobot/config/schema.py | 11 +++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/nanobot/channels/matrix.py b/nanobot/channels/matrix.py index 340ec2a9a..7e8462695 100644 --- a/nanobot/channels/matrix.py +++ b/nanobot/channels/matrix.py @@ -49,9 +49,28 @@ class MatrixChannel(BaseChannel): self._sync_task = asyncio.create_task(self._sync_loop()) async def stop(self) -> None: + """Stop the Matrix channel with graceful sync shutdown.""" self._running = False + + if self.client: + # Request sync_forever loop to exit cleanly. + self.client.stop_sync_forever() + if self._sync_task: - self._sync_task.cancel() + try: + await asyncio.wait_for( + asyncio.shield(self._sync_task), + timeout=self.config.sync_stop_grace_seconds, + ) + except asyncio.TimeoutError: + self._sync_task.cancel() + try: + await self._sync_task + except asyncio.CancelledError: + pass + except asyncio.CancelledError: + pass + if self.client: await self.client.close() diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 57a7e03ea..cd37a7237 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -1,6 +1,7 @@ """Configuration schema using Pydantic.""" from pathlib import Path + from pydantic import BaseModel, Field from pydantic_settings import BaseSettings @@ -53,6 +54,8 @@ class MatrixConfig(BaseModel): access_token: str = "" user_id: str = "" # @bot:matrix.org device_id: str = "" + # Max seconds to wait for sync_forever to stop gracefully before cancellation fallback. + sync_stop_grace_seconds: int = 2 allow_from: list[str] = Field(default_factory=list) class ChannelsConfig(BaseModel): @@ -137,12 +140,12 @@ class Config(BaseSettings): providers: ProvidersConfig = Field(default_factory=ProvidersConfig) gateway: GatewayConfig = Field(default_factory=GatewayConfig) tools: ToolsConfig = Field(default_factory=ToolsConfig) - + @property def workspace_path(self) -> Path: """Get expanded workspace path.""" return Path(self.agents.defaults.workspace).expanduser() - + def _match_provider(self, model: str | None = None) -> tuple["ProviderConfig | None", str | None]: """Match provider config and its registry name. Returns (config, spec_name).""" from nanobot.providers.registry import PROVIDERS @@ -175,7 +178,7 @@ class Config(BaseSettings): """Get API key for the given model. Falls back to first available key.""" p = self.get_provider(model) return p.api_key if p else None - + def get_api_base(self, model: str | None = None) -> str | None: """Get API base URL for the given model. Applies default URLs for known gateways.""" from nanobot.providers.registry import find_by_name @@ -190,7 +193,7 @@ class Config(BaseSettings): if spec and spec.is_gateway and spec.default_api_base: return spec.default_api_base return None - + class Config: env_prefix = "NANOBOT_" env_nested_delimiter = "__"