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

18 KiB
Raw Permalink Blame History

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)

# 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)

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):

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):

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):

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:
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

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:
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():

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:

## 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:

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:

if not self._is_user_allowed(user_id):
    return

Update config/adapters.local.yaml to add allowed_users under the slack block:

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():

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