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:
2026-04-23 07:54:01 -06:00
parent 1232490c3b
commit 916f86725d
70 changed files with 10945 additions and 187 deletions

View File

@@ -17,6 +17,7 @@ import asyncio
import signal
import traceback
from dotenv import load_dotenv
from telegram.error import NetworkError as TelegramNetworkError
# Load environment variables from .env file
load_dotenv()
@@ -85,15 +86,35 @@ class BotRunner:
self.runtime = AdapterRuntime(self.agent)
# Wire child safety filter (no-op if child_safety block is absent from config)
try:
from pathlib import Path as _Path
from child_safety import ChildSafetyConfig, ChildSafetyFilter, ChildAuditLogger
_cs_config = ChildSafetyConfig.from_yaml(_Path("config/adapters.local.yaml"))
if _cs_config:
_cs_audit = ChildAuditLogger(_Path("./memory_workspace"))
_cs_audit.cleanup_old_logs(_cs_config.audit_retention_days)
_cs_filter = ChildSafetyFilter(_cs_config, _cs_audit)
self.runtime.add_preprocessor(_cs_filter.preprocess_adapter)
self.runtime.add_postprocessor(_cs_filter.postprocess_adapter)
print(f"[Setup] Child safety filter active for: {_cs_config.restricted_users}")
except Exception as _e:
print(f"[Setup] Child safety filter not loaded: {_e}")
enabled_count = sum(
self._load_adapter(platform)
for platform in _ADAPTER_CLASSES
)
# Load user mappings
# Config format: "platform_userid" — runtime lookup format: "platform:userid"
user_mapping = self.config_loader.get_user_mapping()
for platform_user_id, username in user_mapping.items():
self.runtime.map_user(platform_user_id, username)
if "_" in platform_user_id:
platform, uid = platform_user_id.split("_", 1)
self.runtime.map_user(f"{platform}:{uid}", username)
else:
self.runtime.map_user(platform_user_id, username)
print(f"[Setup] User mapping: {platform_user_id} -> {username}")
if enabled_count == 0:
@@ -147,30 +168,54 @@ class BotRunner:
print("Starting bot...")
print("=" * 60 + "\n")
try:
await self.runtime.start()
_startup_delays = [10, 30, 60, 120, 300] # backoff between full restart attempts
# Start scheduler if configured
if self.scheduler:
self.scheduler.start()
print("[Scheduler] Task scheduler started\n")
for attempt, delay in enumerate([0] + _startup_delays, 0):
if self.shutdown_event.is_set():
break
if attempt > 0:
print(f"\n[Reconnect] Waiting {delay}s before restart attempt {attempt}...")
try:
await asyncio.wait_for(self.shutdown_event.wait(), timeout=delay)
break # Shutdown was requested during the wait
except asyncio.TimeoutError:
pass
print(f"[Reconnect] Restarting adapters (attempt {attempt})...")
print("=" * 60)
print("Bot is running! Press Ctrl+C to stop.")
print("=" * 60 + "\n")
try:
await self.runtime.start()
# Wait for shutdown signal
await self.shutdown_event.wait()
# Start scheduler if configured
if self.scheduler:
self.scheduler.start()
print("[Scheduler] Task scheduler started\n")
except Exception as e:
print(f"\n[Error] {e}")
traceback.print_exc()
finally:
if self.scheduler:
self.scheduler.stop()
print("[Scheduler] Task scheduler stopped")
await self.runtime.stop()
print("\n[Shutdown] Bot stopped cleanly")
print("=" * 60)
print("Bot is running! Press Ctrl+C to stop.")
print("=" * 60 + "\n")
# Wait for shutdown signal
await self.shutdown_event.wait()
break # Clean shutdown — don't retry
except TelegramNetworkError as e:
print(f"\n[Reconnect] Telegram network error during startup: {e}")
if attempt >= len(_startup_delays):
print("[Reconnect] Max retries reached. Giving up.")
traceback.print_exc()
break
# Will retry in next loop iteration
except Exception as e:
print(f"\n[Error] {e}")
traceback.print_exc()
break # Non-network errors are not retried
finally:
if self.scheduler:
self.scheduler.stop()
print("[Scheduler] Task scheduler stopped")
await self.runtime.stop()
print("\n[Shutdown] Bot stopped cleanly")
async def health_check(self) -> None:
"""Check health of all adapters."""