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,17 +5,19 @@ tasks:
|
||||
# Morning briefing - sent to Slack/Telegram
|
||||
- name: morning-weather
|
||||
prompt: |
|
||||
Check the user profile (Jordan.md) for the location (Centennial, CO). Use the get_weather tool with OpenWeatherMap API to fetch the current weather.
|
||||
|
||||
Also use web_fetch to get today's high/low from a weather service:
|
||||
Fetch the current weather for Centennial, CO using web_fetch:
|
||||
https://wttr.in/Centennial,CO?format=j1
|
||||
|
||||
|
||||
Parse the JSON response to extract:
|
||||
- maxtempF (today's high)
|
||||
- mintempF (today's low)
|
||||
|
||||
- current_condition[0].temp_F (current temp)
|
||||
- current_condition[0].FeelsLikeF (feels like)
|
||||
- weather[0].maxtempF (today's high)
|
||||
- weather[0].mintempF (today's low)
|
||||
- current_condition[0].weatherDesc[0].value (conditions)
|
||||
- current_condition[0].windspeedMiles (wind speed)
|
||||
|
||||
Format the report as:
|
||||
|
||||
|
||||
🌤️ **Weather Report for Centennial, CO**
|
||||
- Current: [current]°F (feels like [feels_like]°F)
|
||||
- Today's High: [high]°F
|
||||
@@ -23,7 +25,7 @@ tasks:
|
||||
- Conditions: [conditions]
|
||||
- Wind: [wind speed] mph
|
||||
- Recommendation: [brief clothing/activity suggestion]
|
||||
|
||||
|
||||
Keep it brief and friendly!
|
||||
schedule: "daily 06:00"
|
||||
enabled: true
|
||||
@@ -59,7 +61,7 @@ tasks:
|
||||
send_to_platform: "telegram"
|
||||
send_to_channel: "8088983654"
|
||||
|
||||
# Daily API cost report
|
||||
# Daily API cost report — DISABLED (bot runs on Max subscription, no per-token billing)
|
||||
- name: daily-cost-report
|
||||
prompt: |
|
||||
Generate a daily API usage and cost report:
|
||||
@@ -88,6 +90,181 @@ tasks:
|
||||
|
||||
Keep it clear and actionable!
|
||||
schedule: "daily 23:00"
|
||||
enabled: false
|
||||
send_to_platform: "telegram"
|
||||
send_to_channel: "8088983654"
|
||||
|
||||
# RSO Phase 2 — Weekly Reflection Agent
|
||||
- name: rso-weekly-reflection
|
||||
prompt: |
|
||||
You are the Weekly Reflection Agent for the RSO (Reflective Self-Optimization) system.
|
||||
Your job is to analyze the past 7 days of interaction logs and produce a structured report.
|
||||
|
||||
## Step 1 — Collect the data
|
||||
|
||||
Run this command to gather and parse the last 7 days of logs:
|
||||
|
||||
```
|
||||
python3 -c "
|
||||
import json, os, glob
|
||||
from datetime import datetime, timedelta
|
||||
from collections import Counter
|
||||
|
||||
log_dir = 'memory_workspace/observation/logs'
|
||||
error_dir = 'memory_workspace/observation/errors'
|
||||
cutoff = datetime.now() - timedelta(days=7)
|
||||
|
||||
interactions, signals, errors = [], [], []
|
||||
|
||||
for path in sorted(glob.glob(f'{log_dir}/*.jsonl')):
|
||||
date_str = os.path.basename(path).replace('.jsonl','')
|
||||
try:
|
||||
file_date = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
if file_date < cutoff:
|
||||
continue
|
||||
except ValueError:
|
||||
continue
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
r = json.loads(line)
|
||||
if r.get('record_type') == 'interaction':
|
||||
interactions.append(r)
|
||||
elif r.get('record_type') == 'signal_patch':
|
||||
signals.append(r)
|
||||
|
||||
for path in sorted(glob.glob(f'{error_dir}/*.jsonl')):
|
||||
date_str = os.path.basename(path).replace('.jsonl','')
|
||||
try:
|
||||
file_date = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
if file_date < cutoff:
|
||||
continue
|
||||
except ValueError:
|
||||
continue
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
errors.append(json.loads(line))
|
||||
|
||||
durations = [r['response']['duration_ms'] for r in interactions]
|
||||
task_types = Counter(r['request']['task_type'] for r in interactions)
|
||||
complexity = Counter(r['request']['complexity'] for r in interactions)
|
||||
all_tools = []
|
||||
for r in interactions:
|
||||
all_tools.extend(t['tool'] for t in r['response'].get('tool_calls', []))
|
||||
tool_counts = Counter(all_tools)
|
||||
|
||||
sig_pos = sum(1 for r in signals if isinstance(r.get('signal'), dict) and r['signal'].get('explicit_positive'))
|
||||
sig_neg = sum(1 for r in signals if isinstance(r.get('signal'), dict) and r['signal'].get('explicit_negative'))
|
||||
sig_correction = sum(1 for r in signals if isinstance(r.get('signal'), dict) and r['signal'].get('correction_followed'))
|
||||
follow_types = Counter(r['signal']['follow_up_type'] for r in signals if isinstance(r.get('signal'), dict))
|
||||
|
||||
slow = [r for r in interactions if r['response']['duration_ms'] > 60000]
|
||||
timeouts = [e for e in errors if 'timed out' in e.get('message','').lower()]
|
||||
|
||||
print('=== STATS ===')
|
||||
print(f'Total interactions: {len(interactions)}')
|
||||
print(f'Total signals: {len(signals)}')
|
||||
print(f'Total errors: {len(errors)}, Timeouts: {len(timeouts)}')
|
||||
print(f'Avg duration: {sum(durations)/len(durations)/1000:.1f}s' if durations else 'No data')
|
||||
print(f'Max duration: {max(durations)/1000:.1f}s' if durations else '')
|
||||
print(f'Slow (>60s): {len(slow)} ({len(slow)*100//len(interactions) if interactions else 0}%)')
|
||||
print(f'Task types: {dict(task_types)}')
|
||||
print(f'Complexity: {dict(complexity)}')
|
||||
print(f'Signals — pos:{sig_pos} neg:{sig_neg} correction:{sig_correction} follow_types:{dict(follow_types)}')
|
||||
print(f'Top 12 tools:')
|
||||
for tool, count in tool_counts.most_common(12):
|
||||
print(f' {tool}: {count}')
|
||||
print(f'Slow interactions:')
|
||||
for r in sorted(slow, key=lambda x: x['response']['duration_ms'], reverse=True)[:10]:
|
||||
print(f' {r[\"response\"][\"duration_ms\"]//1000}s — {r[\"request\"][\"message_preview\"][:70]}')
|
||||
print(f'Errors:')
|
||||
for e in errors:
|
||||
print(f' [{e.get(\"timestamp\",\"\")[:10]}] {e.get(\"error_type\",\"?\")} — {e.get(\"message\",\"\")[:120]}')
|
||||
"
|
||||
```
|
||||
|
||||
## Step 2 — Run the memory relevance scorer
|
||||
|
||||
Run this to score all indexed memory files:
|
||||
|
||||
```
|
||||
python -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
from observation.memory_scorer import MemoryRelevanceScorer
|
||||
scorer = MemoryRelevanceScorer('./memory_workspace')
|
||||
report = scorer.score_all(lookback_days=30)
|
||||
s = report['summary']
|
||||
print('=== MEMORY SCORES ===')
|
||||
print(f'Files scored: {report[\"files_scored\"]} cold_start: {report[\"cold_start\"]}')
|
||||
print(f'Core: {s[\"core_memory\"]} Active: {s[\"active_memory\"]} Archive: {s[\"archive_candidates\"]} Stale: {s[\"stale_candidates\"]}')
|
||||
for e in report['archive_recommendations'][:10]:
|
||||
flags = ','.join(e['staleness_flags']) or 'none'
|
||||
print(f' ARCHIVE {e[\"path\"]} score={e[\"score\"]:.1f} age={e[\"age_days\"]:.0f}d [{flags}]')
|
||||
scorer.write_report(lookback_days=30)
|
||||
"
|
||||
```
|
||||
|
||||
## Step 3 — Write the analysis report
|
||||
|
||||
Using the stats above, write a report answering these five questions. Be specific — use the actual numbers.
|
||||
|
||||
**Q1: What went well?**
|
||||
- Interactions with positive signals and fast completions
|
||||
- Tools and task types that completed efficiently
|
||||
|
||||
**Q2: What went wrong?**
|
||||
- Timeouts, errors, corrections from the user
|
||||
- Any recurring failure patterns
|
||||
|
||||
**Q3: What patterns emerged?**
|
||||
- Most common task types and tools
|
||||
- Any repeated tool chains worth noting
|
||||
|
||||
**Q4: What is being wasted?**
|
||||
- Slow interactions that could be faster
|
||||
- Redundant tool usage patterns
|
||||
- Any scheduled tasks producing no value
|
||||
- Include memory archive candidates from Step 2 (files with score <3 and age >=30d)
|
||||
|
||||
**Q5: Recommendations (3–5 max, data-backed)**
|
||||
- Each one tagged: prompt | tool_usage | memory | code | config
|
||||
- Include the supporting number ("X of Y interactions showed...")
|
||||
- If there are memory archive candidates, include a `memory` recommendation
|
||||
|
||||
## Step 4 — Save the report
|
||||
|
||||
Determine the ISO week number:
|
||||
```
|
||||
python3 -c "from datetime import datetime; d=datetime.now(); print(f'week-{d.year}-{d.isocalendar()[1]:02d}')"
|
||||
```
|
||||
|
||||
Save the report to:
|
||||
- `memory_workspace/observation/summaries/week-YYYY-WW.md` (use write_file)
|
||||
- Obsidian: use permanent_note to save it under `Projects/RSO/Reflections/week-YYYY-WW`
|
||||
|
||||
## Step 5 — Send Telegram summary
|
||||
|
||||
Send a brief summary (not the full report) to Jordan:
|
||||
|
||||
Weekly Reflection Report -- Week WW
|
||||
|
||||
[X] interactions | [Y]% positive signals | [Z] timeouts
|
||||
Memory: [A] archive candidates, [B] stale files
|
||||
|
||||
Top findings:
|
||||
- [Finding 1]
|
||||
- [Finding 2]
|
||||
- [Finding 3]
|
||||
|
||||
Full report saved to Obsidian -> Projects/RSO/Reflections/week-WW
|
||||
|
||||
schedule: "weekly sun 20:00"
|
||||
enabled: true
|
||||
send_to_platform: "telegram"
|
||||
send_to_channel: "8088983654"
|
||||
|
||||
Reference in New Issue
Block a user