From 7c33d3cbe241012eff5f5337912a2e994f0ceba1 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 | 22 +++++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/nanobot/channels/matrix.py b/nanobot/channels/matrix.py index 340ec2a..7e84626 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 8413f0a..f8d251b 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -27,7 +27,9 @@ class TelegramConfig(Base): enabled: bool = False token: str = "" # Bot token from @BotFather allow_from: list[str] = Field(default_factory=list) # Allowed user IDs or usernames - proxy: str | None = None # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080" + proxy: str | None = ( + None # HTTP/SOCKS5 proxy URL, e.g. "http://127.0.0.1:7890" or "socks5://127.0.0.1:1080" + ) class FeishuConfig(Base): @@ -68,6 +70,8 @@ class MatrixConfig(Base): 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) @@ -95,7 +99,9 @@ class EmailConfig(Base): from_address: str = "" # Behavior - auto_reply_enabled: bool = True # If false, inbound email is read but no automatic reply is sent + auto_reply_enabled: bool = ( + True # If false, inbound email is read but no automatic reply is sent + ) poll_interval_seconds: int = 30 mark_seen: bool = True max_body_chars: int = 12000 @@ -172,7 +178,9 @@ class QQConfig(Base): enabled: bool = False app_id: str = "" # 机器人 ID (AppID) from q.qq.com secret: str = "" # 机器人密钥 (AppSecret) from q.qq.com - allow_from: list[str] = Field(default_factory=list) # Allowed user openids (empty = public access) + allow_from: list[str] = Field( + default_factory=list + ) # Allowed user openids (empty = public access) class ChannelsConfig(Base): @@ -231,7 +239,9 @@ class ProvidersConfig(Base): moonshot: ProviderConfig = Field(default_factory=ProviderConfig) minimax: ProviderConfig = Field(default_factory=ProviderConfig) aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway - siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow (硅基流动) API gateway + siliconflow: ProviderConfig = Field( + default_factory=ProviderConfig + ) # SiliconFlow (硅基流动) API gateway openai_codex: ProviderConfig = Field(default_factory=ProviderConfig) # OpenAI Codex (OAuth) github_copilot: ProviderConfig = Field(default_factory=ProviderConfig) # Github Copilot (OAuth) @@ -294,7 +304,9 @@ class Config(BaseSettings): """Get expanded workspace path.""" return Path(self.agents.defaults.workspace).expanduser() - def _match_provider(self, model: str | None = None) -> tuple["ProviderConfig | None", str | None]: + 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