Files
Jordan Ramos 916f86725d 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
2026-04-23 07:54:01 -06:00

451 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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