feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements: - RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector - Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection - Rich conversation storage for notable turns; compact_conversation truncates long user messages - Task-type classifier (query/action/analysis/creative) for observation tagging - Nested sub-agent visibility: deep delegations now register against the main agent's manager Child safety (Gabriel profile): - child_safety.py: filtering, audit logging, prompt constants for restricted sessions - .kiro/specs/child-safety-profile: requirements, design, tasks specs - GABRIEL_BOT_PROPOSAL.md: initial proposal doc - Reduced context window (10 msgs) and tutor-mode identity for restricted users Telegram adapter: - Polling watchdog: auto-restarts updater if polling drops unexpectedly - get_me() with exponential-backoff retry on NetworkError at startup - Correct stop() ordering: signal watchdog before cancelling tasks Email / Gmail: - send_email: supports file attachments (attachments list param) - get_email: surfaces attachment metadata in response Scheduled tasks / weather: - Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively - New scheduled tasks and scheduler state persistence Discord: - adapters/discord/__init__.py scaffold - discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config) Infrastructure: - n8n workflow exports (garvis_webhook, content_pipeline variants) - memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs - UCS C240 migration plan doc - requirements.txt: new deps - .claude/settings.json, fix_hooks.py: hook/permission tuning
This commit is contained in:
@@ -5,10 +5,11 @@ Uses python-telegram-bot library for async Telegram Bot API integration.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from telegram import Bot, Update
|
||||
from telegram.error import TelegramError
|
||||
from telegram.error import NetworkError, TelegramError
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
@@ -39,11 +40,15 @@ class TelegramAdapter(BaseAdapter):
|
||||
- parse_mode: "HTML" or "Markdown" (default: "Markdown")
|
||||
"""
|
||||
|
||||
_RECONNECT_DELAYS = [5, 15, 30, 60, 120] # seconds between startup retries
|
||||
|
||||
def __init__(self, config: AdapterConfig) -> None:
|
||||
super().__init__(config)
|
||||
self.application: Optional[Application] = None
|
||||
self.bot: Optional[Bot] = None
|
||||
self._polling_task: Optional[asyncio.Task] = None
|
||||
self._watchdog_task: Optional[asyncio.Task] = None
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def platform_name(self) -> str:
|
||||
@@ -70,7 +75,7 @@ class TelegramAdapter(BaseAdapter):
|
||||
return bool(bot_token and len(bot_token) > 20)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the Telegram bot."""
|
||||
"""Start the Telegram bot with retry on network failures."""
|
||||
if not self.validate_config():
|
||||
raise ValueError(
|
||||
"Invalid Telegram configuration. Need bot_token"
|
||||
@@ -89,36 +94,84 @@ class TelegramAdapter(BaseAdapter):
|
||||
await self.application.initialize()
|
||||
await self.application.start()
|
||||
|
||||
# Run polling in a background task instead of blocking
|
||||
self._polling_task = asyncio.create_task(
|
||||
self.application.updater.start_polling(
|
||||
allowed_updates=Update.ALL_TYPES,
|
||||
drop_pending_updates=True,
|
||||
)
|
||||
# start_polling() starts internal Updater machinery and returns quickly.
|
||||
# Use updater.running (not task state) to detect if polling is alive.
|
||||
await self.application.updater.start_polling(
|
||||
allowed_updates=Update.ALL_TYPES,
|
||||
drop_pending_updates=True,
|
||||
)
|
||||
|
||||
self.is_running = True
|
||||
|
||||
me = await self.bot.get_me()
|
||||
print(
|
||||
f"[Telegram] Bot started: @{me.username} ({me.first_name})"
|
||||
)
|
||||
# Verify connectivity with retries — get_me() fails on DNS errors at boot.
|
||||
me = await self._get_me_with_retry()
|
||||
if me:
|
||||
print(f"[Telegram] Bot started: @{me.username} ({me.first_name})")
|
||||
else:
|
||||
print("[Telegram] Bot started (could not verify identity — network may be degraded)")
|
||||
|
||||
# Watchdog restarts polling if the task dies unexpectedly.
|
||||
self._watchdog_task = asyncio.create_task(self._polling_watchdog())
|
||||
|
||||
async def _get_me_with_retry(self):
|
||||
"""Call get_me() with exponential backoff. Returns None if all attempts fail."""
|
||||
for attempt, delay in enumerate(self._RECONNECT_DELAYS, 1):
|
||||
try:
|
||||
return await self.bot.get_me()
|
||||
except NetworkError as e:
|
||||
if attempt <= len(self._RECONNECT_DELAYS):
|
||||
print(f"[Telegram] Network error on startup (attempt {attempt}): {e} — retrying in {delay}s")
|
||||
await asyncio.sleep(delay)
|
||||
# Last attempt with no retry after
|
||||
try:
|
||||
return await self.bot.get_me()
|
||||
except NetworkError as e:
|
||||
print(f"[Telegram] Could not reach Telegram API after {len(self._RECONNECT_DELAYS)+1} attempts: {e}")
|
||||
return None
|
||||
|
||||
async def _polling_watchdog(self) -> None:
|
||||
"""Monitor the Updater and restart polling if it stops unexpectedly."""
|
||||
# Give polling a moment to fully initialise before we start watching.
|
||||
await asyncio.sleep(15)
|
||||
while self.is_running:
|
||||
await asyncio.sleep(30)
|
||||
if not self.is_running:
|
||||
break
|
||||
if not self.application or self.application.updater.running:
|
||||
continue # All good
|
||||
|
||||
self._logger.warning("[Telegram] Updater is no longer running — attempting restart")
|
||||
print("[Telegram] Polling dropped — restarting...")
|
||||
for attempt, delay in enumerate(self._RECONNECT_DELAYS, 1):
|
||||
await asyncio.sleep(delay)
|
||||
if not self.is_running:
|
||||
return
|
||||
try:
|
||||
await self.application.updater.start_polling(
|
||||
allowed_updates=Update.ALL_TYPES,
|
||||
drop_pending_updates=False,
|
||||
)
|
||||
print(f"[Telegram] Polling restarted (attempt {attempt})")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[Telegram] Restart attempt {attempt} failed: {e}")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the Telegram bot."""
|
||||
self.is_running = False # Signal watchdog to exit before cancelling tasks
|
||||
|
||||
if self._watchdog_task and not self._watchdog_task.done():
|
||||
self._watchdog_task.cancel()
|
||||
try:
|
||||
await self._watchdog_task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
if self.application:
|
||||
print("[Telegram] Stopping bot...")
|
||||
await self.application.updater.stop()
|
||||
await self.application.stop()
|
||||
await self.application.shutdown()
|
||||
self.is_running = False
|
||||
|
||||
if self._polling_task and not self._polling_task.done():
|
||||
self._polling_task.cancel()
|
||||
try:
|
||||
await self._polling_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
print("[Telegram] Bot stopped")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user