The Setup
Apollo Dashboard (my personal AI cockpit β a local Python server that triages my email, Slack, and pipeline into one read/act surface) learns from every π and π I give it.
Over weeks, those clicks accumulate into sender rules and topic preferences β the thing that makes triage feel smart instead of random.
I shipped a /rules page so I could actually inspect what Apollo had learned.
The Wall
Email rules looked great. Sender address, a subject-line snippet, a clear "always/never action" label.
Slack rules?
u07p1ern533.
u04p9fgx112.
Raw Slack user IDs. Every single one. No name. No message snippet. No context for why the rule existed.
I had no idea whose preferences I was looking at.
What Didn't Work First
The obvious fix: look up each ID against my local user cache at state/slack_users.json.
Simple loop. Broke immediately.
The lookup returned nothing because the cache stores keys in UPPERCASE β U07P1ERN533 β but feedback records them in lowercase. Silent miss every time.
Not the kind of bug that throws an error. The kind that just shows you nothing and lets you feel stupid for a minute.
The Fix That Actually Worked
Wrote _name_map() in slack_feedback.py to do a case-insensitive resolve β normalize both sides to uppercase on compare, return the display name as the primary label with the raw ID muted underneath.
Then surfaced the example message text. Slack feedback had already been storing the triggering message in the examples field β I just wasn't showing it. Now the view renders: display name Β· raw id (muted) Β· the actual message that created the rule.
Added the same treatment to email for parity: subject line as context, not just the sender address.
Commit 0034b01.
Then I Read the Rules
And immediately wanted to burn them down.
The rules weren't wrong, exactly. They were just⦠trained on a weird two-week period where I was clicking fast and not paying attention. They didn't reflect how I actually want Apollo to triage anything.
Which is exactly why "Clear All" had to ship in the same commit.
clear_all() in both feedback modules resets the store back to _blank() β threads, prefs, sender rules, topics, all zeroed. A confirm-gated red button on /rules hits POST /api/feedback-clear with {source: email|slack|both}.
Before I hit it, the backend snapshotted both stores β a pre-clear backup, timestamped and sitting on disk. "Start over" doesn't mean "make it impossible to recover."
That's the no-destructive-ops rule in practice: every burn switch needs an ejector seat.
I wiped everything. Both stores to blank. Starting fresh.
Why This Matters to Me
You can't evaluate training data you can't read.
I had weeks of learned preferences sitting in files I literally could not interpret. I thought the rules were accumulating value. They were accumulating noise I couldn't see.
The lesson isn't just "make your admin UI human-readable" β though yes, absolutely, do that. It's that a destructive reset is a first-class feature, not an afterthought. If your AI agent is learning and you can't inspect what it learned AND wipe it safely, you don't have a feedback system. You have a black box that gets more confident over time.
Make it readable. Make it wipeable. Make the wipe reversible.
Then actually look at what you trained.
P.S. Both stores are at zero as of 2026-06-24. The re-training starts now. I'm curious how different the rules look in a month when I'm actually paying attention to what I click.