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

@@ -0,0 +1,450 @@
# Tasks — Child Safety Profile
Implementation order matters. Each task has a dependency note where relevant.
Tasks within a phase can be worked in parallel; phases must be completed in order.
---
## Phase 1 — Identity & Config (no code changes, unblocks everything else)
### TASK-01 — Add gabriel's Slack user ID to config
**File:** `config/adapters.local.yaml`
**What:** Add gabriel's Slack ID to `allowed_users` and `user_mapping`; add `child_safety` block.
**Requires:** Son's actual Slack user ID (parent to provide)
```yaml
# Under slack adapter settings:
allowed_users:
- <JORDANS_SLACK_USER_ID> # Jordan
- <GABRIELS_SLACK_USER_ID> # Gabriel
user_mapping:
slack_<JORDANS_SLACK_USER_ID>: "jordan"
slack_<GABRIELS_SLACK_USER_ID>: "gabriel"
# New top-level block:
child_safety:
restricted_users:
- gabriel
audit_retention_days: 365
```
Note: The Slack adapter has no allow-list today. `allowed_users` only takes effect after
TASK-01b adds the check to `adapters/slack/adapter.py`. Do both together.
**Acceptance:** Bot recognises Gabriel's Slack account and routes him to username `"gabriel"`.
All other Slack users (not in `allowed_users`) are silently ignored.
---
### TASK-02 — Create gabriel's user profile
**File:** `memory_workspace/users/gabriel.md` (new)
**What:** Write the per-user profile that gets injected into the system prompt.
**Depends on:** Nothing
Content to include:
- Age: 13
- Interests: gaming, horror games, Lua scripting, Roblox Studio, game design
- Learning style: hands-on, wants working code examples, short explanations before diving in
- Communication style: casual, encouraging, celebrate what he's building
- Current projects: Roblox horror game (update as known)
- Do NOT include guardrail rules here — those come from the injected block
**Acceptance:** Profile loads without error; content appears in system prompt during gabriel's session (verify via debug log).
---
## Phase 2 — Audit Logger
### TASK-03 — Implement `ChildAuditLogger`
**File:** `child_safety.py` (new — start with this class only)
**What:** Thread-safe, non-blocking JSONL audit logger for child user interactions.
**Depends on:** Nothing (standalone)
Implementation notes:
- Mirror the pattern from `observation/interaction_logger.py` exactly
- Write to `memory_workspace/audit/{username}/YYYY-MM-DD.jsonl`
- Create directory on first write (not at init, to avoid creating dirs for unused usernames)
- Single public method: `log(username, platform, action, filter_stage, filter_reason, message, response)`
- `action` values: `"allowed"` | `"blocked"` | `"flagged"`
- `cleanup_old_logs(retention_days)` — prune files older than retention window
**Acceptance:** Unit-testable in isolation. Call `log()` twice, verify two JSONL lines written
to the correct file path with correct schema.
---
## Phase 3 — Filtering Logic
### TASK-04 — Implement `ChildSafetyConfig`
**File:** `child_safety.py` (add to existing file)
**What:** Config loader that reads the `child_safety` block from `adapters.local.yaml`.
**Depends on:** TASK-01 (config block must exist)
```python
class ChildSafetyConfig:
restricted_users: list[str]
audit_retention_days: int
@classmethod
def from_yaml(cls, config_path: Path) -> "ChildSafetyConfig": ...
def is_restricted(self, username: str) -> bool: ...
```
**Acceptance:** `is_restricted("gabriel")` returns `True`; `is_restricted("cloe")` returns `False`.
---
### TASK-05 — Implement `ChildSafetyFilter` — input (preprocessor)
**File:** `child_safety.py` (add to existing file)
**What:** Intent-pattern input filter.
**Depends on:** TASK-03, TASK-04
Implementation notes:
- Compile all regex patterns once at class instantiation (not per-call)
- Pattern evaluation order: hard-block → context signals → conditional block → pass
- `preprocess(message: InboundMessage) -> tuple[InboundMessage | None, str | None]`
- Returns `(message, None)` → pass through
- Returns `(None, reply_text)` → block; caller sends reply_text directly
- Call `AuditLogger.log()` with `action="blocked"` on any block
- Call `AuditLogger.log()` with `action="allowed"` on pass (response field left null here —
audit logger will be called again in the postprocessor with the full response)
Hard-block patterns (always active):
```python
HARD_BLOCK_PATTERNS = [
r"\b(sex|porn|nude|naked|explicit)\b",
r"\bhow (do i|to|can i).{0,40}(kill|hurt|stab|shoot|harm).{0,30}(myself|yourself)\b",
r"\bhow (do i|to|can i).{0,40}(hurt|stab|kill|attack|beat up|harm).{0,30}(my |a )?(sister|brother|mom|dad|teacher|classmate|friend|kid|child|person|someone|people)\b",
r"\b(give me|what is|find).{0,30}(address|phone number|school|location).{0,30}(of|for)\b",
]
```
Game context signals (exempts from conditional blocks):
```python
GAME_CONTEXT_SIGNALS = [
r"\bin (my |the |a )?(game|roblox|studio|script|map|level|world|place)\b",
r"\b(lua|roblox|studio|npc|hitbox|raycast|humanoid|workspace|basepart|tool|part)\b",
r"\b(code|script|function|method|module|class|variable|loop|event|animate|tween)\b",
r"\b(damage|health|respawn|kill|destroy)\b.{0,30}\b(player|npc|enemy|mob|character|humanoid)\b",
r"\bhow (do i|to|can i) (make|get|set|add|create|implement|build|script)\b",
]
```
Conditional block patterns (only active when no game context signal):
```python
CONDITIONAL_BLOCK_PATTERNS = [
r"\bhow (do i|to|can i).{0,40}(use|wield|make|build).{0,30}(knife|gun|pistol|rifle|weapon|sword|bomb).{0,30}(hurt|harm|attack|fight|cut|stab|shoot)\b",
r"\bhow (do i|to|can i).{0,40}(hurt|fight|attack|beat).{0,30}(someone|people|person|kid|child)\b",
r"\b(buy|get|obtain|find).{0,30}(drugs?|weed|cocaine|meth|pills)\b",
]
```
**Acceptance:**
- `"how do I make a knife swing animation in Roblox"` → pass (game context)
- `"how do I use a knife to hurt someone"` → block (no game context, conditional pattern)
- `"how do I kill all NPCs in my game"` → pass (game context signal: NPC)
- `"how do I hurt my classmate"` → block (hard block)
- `"lua script for a gun that shoots"` → pass (game context)
---
### TASK-06 — Implement `ChildSafetyFilter` — output (postprocessor)
**File:** `child_safety.py` (add to existing file)
**What:** Light response scan before delivery.
**Depends on:** TASK-05
Implementation notes:
- `postprocess(response: str, message: InboundMessage) -> str`
- Scan for: explicit terms list, profanity list, real-world harm instruction patterns
- If flagged: log `action="flagged"`, return safe fallback string
- If clean: log `action="allowed"` with full response (this is the final audit entry)
- Fallback string: "I ran into a bit of a snag there. Try rephrasing, or ask me something
about your Roblox game — I love helping with scripts and game design!"
**Acceptance:**
- Clean Lua code response → returned unchanged
- Response containing explicit term → replaced with fallback
---
## Phase 4 — System Prompt Injection
### TASK-07 — Add guardrail block injection to `agent.py`
**File:** `agent.py`, `_build_system_prompt()` (~line 488)
**What:** Inject `CHILD_GUARDRAIL_BLOCK` for restricted users.
**Depends on:** TASK-04
Changes:
1. Import `ChildSafetyConfig` and `CHILD_GUARDRAIL_BLOCK` from `child_safety`
2. In `Agent.__init__()`, load config and store as `self._child_safety_config`
3. In `_build_system_prompt()`, after the user profile block:
```python
if (not self.is_sub_agent
and self._child_safety_config
and self._child_safety_config.is_restricted(username)):
system_parts.append(CHILD_GUARDRAIL_BLOCK)
```
`CHILD_GUARDRAIL_BLOCK` is a module-level constant in `child_safety.py`.
**Acceptance:** Add a debug print temporarily — verify the guardrail block appears in the
assembled system prompt when username is `"gabriel"` and does not appear for `"cloe"`.
---
## Phase 5 — Runtime Wiring
### TASK-08 — Register pre/postprocessors in `adapters/runtime.py`
**File:** `adapters/runtime.py`, `AdapterRuntime.__init__()`
**What:** Instantiate the filter and register it with the runtime.
**Depends on:** TASK-05, TASK-06
```python
from child_safety import ChildSafetyConfig, ChildSafetyFilter, ChildAuditLogger
_cs_config = ChildSafetyConfig.from_yaml(config_path)
_cs_audit = ChildAuditLogger(workspace_dir)
_cs_audit.cleanup_old_logs(_cs_config.audit_retention_days)
_cs_filter = ChildSafetyFilter(_cs_config, _cs_audit)
self.add_preprocessor(_cs_filter.preprocess_adapter)
self.add_postprocessor(_cs_filter.postprocess_adapter)
```
The `preprocess_adapter` and `postprocess_adapter` wrappers adapt the filter's return types
to match the runtime's expected preprocessor/postprocessor signatures.
For blocking: the preprocessor adapter queues the safe reply via the adapter's `send_message()`
and returns a sentinel `InboundMessage` that causes the agent to be skipped. Review the runtime
queue loop in `_process_message()` to confirm the cleanest abort point.
**Acceptance:** End-to-end test with gabriel's Slack account (or mocked username):
- Allowed message → full response delivered, audit entry written
- Blocked message → canned reply delivered immediately, no LLM call made
---
## Phase 6 — Verification
### TASK-09 — End-to-end smoke test
**What:** Manual testing checklist before considering the feature complete.
- [ ] Gabriel's Slack ID is mapped and he can message the bot
- [ ] Allowed game dev question gets a helpful Lua/Roblox response
- [ ] Blocked real-world harm question gets canned reply, no LLM call
- [ ] Horror game question with violence words (e.g., "enemy takes damage") passes through
- [ ] Audit log file created at correct path with correct schema
- [ ] Parent (Jordan/Cloe) messages are completely unaffected — full SOUL.md + context.md injected as normal
- [ ] RSO interaction log unchanged (no extra entries for blocked messages)
- [ ] Bot restart preserves config (no in-memory-only state)
- [ ] Gabriel's system prompt does NOT contain SOUL.md or context.md content (verify via debug)
- [ ] Jordan's system prompt still contains SOUL.md and context.md (verify no regression)
### TASK-10 — Review and finalise Gabriel's user profile
**File:** `memory_workspace/users/gabriel.md`
**What:** After first real interactions, update the profile with observed interests, current
project details, preferred explanation style. This is an ongoing task.
**Depends on:** TASK-09
---
## Phase 7 — Token Optimization
### TASK-11 — Implement stripped system prompt for restricted users
**File:** `agent.py`, `_build_system_prompt()` and `_chat_inner()`
**What:** Build the child-optimized system prompt path; reduce history window for restricted users.
**Depends on:** TASK-07 (child safety config already wired into Agent)
Changes to `_build_system_prompt()`:
1. Add module-level constant `CHILD_TUTOR_IDENTITY` — ~100-token identity replacing SOUL.md
2. Add module-level constant `CHILD_MAX_CONTEXT_MESSAGES = 10`
3. When `is_restricted(username)` is true, build a stripped prompt:
```python
def _build_child_system_prompt(self, username: str) -> str:
user_profile = self.memory.get_user(username)
parts = [
CHILD_TUTOR_IDENTITY,
f"User Profile:\n{user_profile}",
CHILD_GUARDRAIL_BLOCK,
"You have access to tools for web search and code help. "
"Use them to assist Gabriel with his game development questions.",
]
return "\n\n".join(parts)
```
Note: `get_soul()`, `get_context()`, `search_hybrid()`, and the delegation block are
all deliberately absent.
Changes to `_chat_inner()`:
```python
is_child = (self._child_safety_config
and self._child_safety_config.is_restricted(username))
cap = CHILD_MAX_CONTEXT_MESSAGES if is_child else MAX_CONTEXT_MESSAGES
system = (self._build_child_system_prompt(username) if is_child
else self._build_system_prompt(user_message, username, platform))
```
**Acceptance:**
- Gabriel's assembled system prompt: no SOUL.md text, no SSH/Proxmox content, no delegation block
- Gabriel's history window: max 10 messages passed to LLM
- Jordan's assembled system prompt: unchanged (full SOUL.md + context.md present)
- Measure approximate token count difference (log `len(system)` for both users)
### TASK-12 — Add AI literacy guidance to gabriel.md
**File:** `memory_workspace/users/gabriel.md`
**What:** Add a section explicitly coaching the model on how to teach Gabriel to use the bot
well — question framing, context windows, spotting assumptions. This lives in the profile (not
the guardrail block) so it can be updated over time as his skills grow.
**Depends on:** TASK-02 (profile already exists)
Add a section to gabriel.md:
```markdown
## Teaching Him to Use AI Well
Help Gabriel get better at using AI tools as a skill in itself. Do this naturally, not as
a lesson — just model good practice and name it when it happens.
- When he asks something vague: clarify first, then answer. Name what you're doing.
"Just checking what you mean first — that's a good habit when asking any AI."
- When context runs out or he notices you "forgot": explain the context window simply.
"I can only hold so much conversation at once — like a whiteboard that fills up."
- When he nails a well-structured question: tell him.
"That was a perfect question — gave me exactly what I needed."
- Teach the format: what I have / what I want / what I've tried.
- Flag your own assumptions visibly so he learns to spot ambiguity in questions.
```
**Acceptance:** Profile updated; the AI literacy guidance appears in Gabriel's system prompt
on the next session.
---
## Summary Table
| Task | File | Phase | Depends On |
|------|------|-------|-----------|
| TASK-01 | `config/adapters.local.yaml` | 1 — Identity | Parent provides Slack ID |
| TASK-02 | `memory_workspace/users/gabriel.md` | 1 — Identity | — |
| TASK-03 | `child_safety.py` | 2 — Audit | — |
| TASK-04 | `child_safety.py` | 3 — Filter | TASK-01 |
| TASK-05 | `child_safety.py` | 3 — Filter | TASK-03, TASK-04 |
| TASK-06 | `child_safety.py` | 3 — Filter | TASK-05 |
| TASK-07 | `agent.py` | 4 — System Prompt | TASK-04 |
| TASK-08 | `adapters/runtime.py` | 5 — Wiring | TASK-05, TASK-06 |
| TASK-09 | — | 6 — Verify | All |
| TASK-10 | `memory_workspace/users/gabriel.md` | 6 — Verify | TASK-09 |
| TASK-11 | `agent.py` | 7 — Token Optimization | TASK-07 |
| TASK-12 | `memory_workspace/users/gabriel.md` | 7 — Token Optimization | TASK-02 |
| TASK-13 | `adapters/slack/adapter.py` | 8 — Slack Allow-List | — |
| TASK-14 | `agent.py`, `gabriel_context.md` | 9 — Continuity | TASK-11 |
| TASK-15 | `agent.py` | 9 — Continuity | TASK-14 |
| TASK-16 | — | 10 — Full Verify | All |
---
## Phase 8 — Slack Allow-List
### TASK-13 — Add allow-list check to Slack adapter
**File:** `adapters/slack/adapter.py`
**What:** Silently drop messages from users not in `allowed_users` config.
**Depends on:** Nothing (standalone fix)
Add to `SlackAdapter`:
```python
def _is_user_allowed(self, user_id: str) -> bool:
allowed = self.config.settings.get("allowed_users", [])
if not allowed:
return True # open if unconfigured — preserves existing behaviour
return str(user_id) in [str(u) for u in allowed]
```
In `handle_message_events()`, first line after extracting `user_id`:
```python
if not self._is_user_allowed(user_id):
return
```
Update `config/adapters.local.yaml` to add `allowed_users` under the slack block:
```yaml
slack:
allowed_users:
- U01234JORDAN # Jordan's Slack user ID (find via Slack profile → More → Copy member ID)
- U09876GABRIEL # Gabriel's Slack user ID
```
**Acceptance:** A Slack message from a user not in the list produces no bot response.
Jordan and Gabriel's messages continue to work normally.
---
## Phase 9 — Cross-Session Continuity & First-Run
### TASK-14 — Implement gabriel_context.md injection and end-of-session update
**File:** `agent.py`, `_build_child_system_prompt()`
**What:** Inject `gabriel_context.md` into Gabriel's system prompt; instruct the bot to
update it at the end of each session.
**Depends on:** TASK-11 (child system prompt path)
Changes to `_build_child_system_prompt()`:
```python
context_path = self.memory.workspace_dir / "users" / "gabriel_context.md"
gabriel_context = context_path.read_text(encoding="utf-8") if context_path.exists() else None
is_first_run = gabriel_context is None
parts = [CHILD_TUTOR_IDENTITY, f"User Profile:\n{user_profile}"]
if gabriel_context:
parts.append(f"Project Context & Skills:\n{gabriel_context}")
if is_first_run:
parts.append(FIRST_RUN_BLOCK) # see TASK-15
parts.append(CHILD_GUARDRAIL_BLOCK)
parts.append(SESSION_UPDATE_INSTRUCTION) # always appended
parts.append("You have access to file tools. Use them to update gabriel_context.md "
"at the end of this conversation.")
```
`SESSION_UPDATE_INSTRUCTION` constant:
```
At the end of this conversation, use your file write tool to update
`memory_workspace/users/gabriel_context.md` with:
- ## Active Project: what Gabriel is building (name + one sentence description)
- ## Last Session (today's date): what was worked on, bugs fixed, concepts covered
- ## Open Threads: anything Gabriel mentioned wanting to do next
- ## Skills Introduced: cumulative list of concepts taught, with date first introduced
Keep the file under 40 lines. Overwrite it completely each time.
```
**Acceptance:**
- Second session: Gabriel's project from previous session appears in system prompt
- File is updated after the session ends (check `gabriel_context.md` was written)
- First session: file absent, bot starts fresh, file created after first exchange
### TASK-15 — Implement first-run welcome
**File:** `agent.py`, `child_safety.py`
**What:** Send a warm onboarding welcome on Gabriel's very first message.
**Depends on:** TASK-14 (first-run detection)
`FIRST_RUN_BLOCK` constant (added to `child_safety.py`):
```
FIRST SESSION: This is Gabriel's very first message. Before answering his question,
send a short friendly welcome (45 sentences max). Cover:
- What you can help with: Lua, Roblox Studio, game design, coding questions
- That you guide and teach rather than just hand over answers
- That you'll remember his projects between sessions
- Invite him to tell you what he's building (or answer if he already has)
Casual and warm — not a formal introduction. Then answer his question normally.
```
This block is appended to `_build_child_system_prompt()` only when `is_first_run is True`.
**Acceptance:**
- First ever message from Gabriel → bot sends welcome then answers the question
- Second session → no welcome, goes straight to the question
- Jordan's sessions → no welcome block ever injected