feat: require warning before mute + sustained toxicity escalation

Gate mutes behind a prior warning — first offense always gets a warning,
mute only fires if warned_since_reset is True. Warned flag is persisted
to DB (new Warned column on UserState) and survives restarts.

Add post-warning escalation boost to drama_score: each high-scoring
message after a warning adds +0.04 (configurable) so sustained bad
behavior ramps toward the mute threshold instead of plateauing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 11:07:57 -05:00
parent f02a4ab49d
commit 71c7b45e9a
4 changed files with 56 additions and 16 deletions

View File

@@ -126,6 +126,12 @@ class Database:
ALTER TABLE UserState ADD UserNotes NVARCHAR(MAX) NULL
""")
# --- Schema migration for warned flag (require warning before mute) ---
cursor.execute("""
IF COL_LENGTH('UserState', 'Warned') IS NULL
ALTER TABLE UserState ADD Warned BIT NOT NULL DEFAULT 0
""")
cursor.execute("""
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
CREATE TABLE BotSettings (
@@ -284,19 +290,20 @@ class Database:
off_topic_count: int,
baseline_coherence: float = 0.85,
user_notes: str | None = None,
warned: bool = False,
) -> None:
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes)."""
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned)."""
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,
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned,
)
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):
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned):
conn = self._connect()
try:
cursor = conn.cursor()
@@ -306,14 +313,14 @@ class Database:
ON target.UserId = source.UserId
WHEN MATCHED THEN
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
BaselineCoherence = ?, UserNotes = ?,
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
UpdatedAt = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes)
VALUES (?, ?, ?, ?, ?, ?);""",
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned)
VALUES (?, ?, ?, ?, ?, ?, ?);""",
user_id,
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes,
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes,
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0,
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0,
)
cursor.close()
finally:
@@ -356,7 +363,7 @@ class Database:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes FROM UserState"
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned FROM UserState"
)
rows = cursor.fetchall()
cursor.close()
@@ -368,6 +375,7 @@ class Database:
"off_topic_count": row[3],
"baseline_coherence": float(row[4]),
"user_notes": row[5] or "",
"warned": bool(row[6]),
}
for row in rows
]