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

@@ -2568,9 +2568,9 @@ async def get_weather(args: Dict[str, Any]) -> Dict[str, Any]:
name="send_email",
description="Send an email via Gmail API. Requires prior OAuth setup (--setup-google).",
description="Send an email via Gmail API. Requires prior OAuth setup (--setup-google). Optionally attach local files by providing their absolute paths.",
input_schema={"to": str, "subject": str, "body": str, "cc": str, "reply_to_message_id": str},
input_schema={"to": str, "subject": str, "body": str, "cc": str, "reply_to_message_id": str, "attachments": list},
)
@@ -2588,6 +2588,8 @@ async def send_email(args: Dict[str, Any]) -> Dict[str, Any]:
reply_to_message_id = args.get("reply_to_message_id")
attachments = args.get("attachments") or []
gmail_client, _, _ = _initialize_google_clients()
@@ -2612,6 +2614,8 @@ async def send_email(args: Dict[str, Any]) -> Dict[str, Any]:
reply_to_message_id=reply_to_message_id,
attachments=attachments or None,
)
@@ -2620,7 +2624,9 @@ async def send_email(args: Dict[str, Any]) -> Dict[str, Any]:
msg_id = result.get("message_id", "unknown")
text = f"Email sent successfully to {to}\nMessage ID: {msg_id}\nSubject: {subject}"
attach_note = f"\nAttachments: {len(attachments)} file(s)" if attachments else ""
text = f"Email sent successfully to {to}\nMessage ID: {msg_id}\nSubject: {subject}{attach_note}"
else:
@@ -2740,6 +2746,24 @@ async def get_email(args: Dict[str, Any]) -> Dict[str, Any]:
email_data = result.get("email", {})
attachments = email_data.get("attachments", [])
if attachments:
attach_lines = "\n".join(
f" [{i+1}] {a['filename']} ({a['mime_type']}, {a['size']} bytes) — attachment_id: {a['attachment_id']}"
for i, a in enumerate(attachments)
)
attach_section = f"\n\nAttachments ({len(attachments)}):\n{attach_lines}"
else:
attach_section = ""
text = (
f"From: {email_data.get('from', 'Unknown')}\n"
@@ -2748,7 +2772,9 @@ async def get_email(args: Dict[str, Any]) -> Dict[str, Any]:
f"Subject: {email_data.get('subject', 'No subject')}\n"
f"Date: {email_data.get('date', 'Unknown')}\n\n"
f"Date: {email_data.get('date', 'Unknown')}"
f"{attach_section}\n\n"
f"{email_data.get('body', 'No content')}"
@@ -2769,6 +2795,91 @@ async def get_email(args: Dict[str, Any]) -> Dict[str, Any]:
@tool(
name="download_attachment",
description=(
"Download an email attachment to disk. "
"Use get_email first to retrieve the message_id and attachment_id. "
"Returns the local file path where the attachment was saved."
),
input_schema={"message_id": str, "attachment_id": str, "filename": str, "output_dir": str},
)
async def download_attachment(args: Dict[str, Any]) -> Dict[str, Any]:
"""Download an email attachment to disk."""
message_id = args["message_id"]
attachment_id = args["attachment_id"]
filename = args["filename"]
output_dir = args.get("output_dir", "downloads")
gmail_client, _, _ = _initialize_google_clients()
if not gmail_client:
return {
"content": [{"type": "text", "text": "Error: Google not authorized. Run: python bot_runner.py --setup-google"}],
"isError": True
}
result = gmail_client.download_attachment(
message_id=message_id,
attachment_id=attachment_id,
filename=filename,
output_dir=output_dir,
)
if result["success"]:
text = (
f"Attachment downloaded: {result['filename']}\n"
f"Saved to: {result['file_path']}\n"
f"Size: {result['size']} bytes"
)
else:
text = f"Error downloading attachment: {result.get('error', 'Unknown error')}"
return {"content": [{"type": "text", "text": text}], "isError": not result["success"]}
@tool(
@@ -4131,6 +4242,7 @@ file_system_server = create_sdk_mcp_server(
send_email,
read_emails,
get_email,
download_attachment,
# Calendar tools
read_calendar,
create_calendar_event,