feat: move user aliases from config to DB with /bcs-alias command

Aliases now stored in UserState table instead of config.yaml. Adds
Aliases column (NVARCHAR 500), loads on startup, persists via flush.
New /bcs-alias slash command (view/set/clear) for managing nicknames.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 10:35:19 -05:00
parent ad1234ec99
commit 33d56f8737
6 changed files with 124 additions and 28 deletions

View File

@@ -138,6 +138,12 @@ class Database:
ALTER TABLE UserState ADD LastOffenseAt FLOAT NULL
""")
# --- Schema migration for user aliases/nicknames ---
cursor.execute("""
IF COL_LENGTH('UserState', 'Aliases') IS NULL
ALTER TABLE UserState ADD Aliases NVARCHAR(500) NULL
""")
cursor.execute("""
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
CREATE TABLE BotSettings (
@@ -314,19 +320,20 @@ class Database:
user_notes: str | None = None,
warned: bool = False,
last_offense_at: float | None = None,
aliases: str | None = None,
) -> None:
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time)."""
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time, aliases)."""
if not self._available:
return
try:
await asyncio.to_thread(
self._save_user_state_sync,
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at,
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at, aliases,
)
except Exception:
logger.exception("Failed to save user state")
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at):
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at, aliases):
conn = self._connect()
try:
cursor = conn.cursor()
@@ -337,14 +344,14 @@ class Database:
WHEN MATCHED THEN
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
LastOffenseAt = ?,
LastOffenseAt = ?, Aliases = ?,
UpdatedAt = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);""",
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);""",
user_id,
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at,
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at,
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at, aliases,
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at, aliases,
)
cursor.close()
finally:
@@ -387,7 +394,7 @@ class Database:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt FROM UserState"
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases FROM UserState"
)
rows = cursor.fetchall()
cursor.close()
@@ -401,6 +408,7 @@ class Database:
"user_notes": row[5] or "",
"warned": bool(row[6]),
"last_offense_at": float(row[7]) if row[7] is not None else 0.0,
"aliases": row[8] or "",
}
for row in rows
]

View File

@@ -30,6 +30,8 @@ class UserDrama:
last_coherence_alert_time: float = 0.0
# Per-user LLM notes
notes: str = ""
# Known aliases/nicknames
aliases: list[str] = field(default_factory=list)
class DramaTracker:
@@ -217,6 +219,16 @@ class DramaTracker:
def clear_user_notes(self, user_id: int) -> None:
self.get_user(user_id).notes = ""
def get_user_aliases(self, user_id: int) -> list[str]:
return self.get_user(user_id).aliases
def set_user_aliases(self, user_id: int, aliases: list[str]) -> None:
self.get_user(user_id).aliases = aliases
def get_all_aliases(self) -> dict[int, list[str]]:
"""Return {user_id: [aliases]} for all users that have aliases set."""
return {uid: user.aliases for uid, user in self._users.items() if user.aliases}
def reset_off_topic(self, user_id: int) -> None:
user = self.get_user(user_id)
user.off_topic_count = 0
@@ -298,6 +310,8 @@ class DramaTracker:
user.offense_count = 0
user.warned_since_reset = False
user.last_offense_time = 0.0
if state.get("aliases"):
user.aliases = [a.strip() for a in state["aliases"].split(",") if a.strip()]
count += 1
return count