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
18 KiB
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.pyexactly - 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) actionvalues:"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
- Returns
- Call
AuditLogger.log()withaction="blocked"on any block - Call
AuditLogger.log()withaction="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:
- Import
ChildSafetyConfigandCHILD_GUARDRAIL_BLOCKfromchild_safety - In
Agent.__init__(), load config and store asself._child_safety_config - 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():
- Add module-level constant
CHILD_TUTOR_IDENTITY— ~100-token identity replacing SOUL.md - Add module-level constant
CHILD_MAX_CONTEXT_MESSAGES = 10 - 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.mdwas 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 (4–5 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