feat: add warning expiration and exclude moderated messages from context

Warning flag now auto-expires after a configurable duration
(warning_expiration_minutes, default 30m). After expiry, the user must
be re-warned before a mute can be issued.

Messages that triggered moderation actions (warnings/mutes) are now
excluded from the LLM context window in both buffered analysis and
mention scans, preventing already-actioned content from influencing
future scoring. Uses in-memory tracking plus bot reaction fallback
for post-restart coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 13:39:49 -05:00
parent 36df4cf5a6
commit eb7eb81621
6 changed files with 86 additions and 18 deletions

View File

@@ -144,6 +144,12 @@ class Database:
ALTER TABLE UserState ADD Aliases NVARCHAR(500) NULL
""")
# --- Schema migration for warning expiration ---
cursor.execute("""
IF COL_LENGTH('UserState', 'WarningExpiresAt') IS NULL
ALTER TABLE UserState ADD WarningExpiresAt FLOAT NULL
""")
cursor.execute("""
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
CREATE TABLE BotSettings (
@@ -321,19 +327,20 @@ class Database:
warned: bool = False,
last_offense_at: float | None = None,
aliases: str | None = None,
warning_expires_at: float | None = None,
) -> None:
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time, aliases)."""
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time, aliases, warning expiration)."""
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, aliases,
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at, aliases, warning_expires_at,
)
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, aliases):
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at, aliases, warning_expires_at):
conn = self._connect()
try:
cursor = conn.cursor()
@@ -344,14 +351,14 @@ class Database:
WHEN MATCHED THEN
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
LastOffenseAt = ?, Aliases = ?,
LastOffenseAt = ?, Aliases = ?, WarningExpiresAt = ?,
UpdatedAt = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);""",
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases, WarningExpiresAt)
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, 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,
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at, aliases, warning_expires_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, aliases, warning_expires_at,
)
cursor.close()
finally:
@@ -394,7 +401,7 @@ class Database:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases FROM UserState"
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases, WarningExpiresAt FROM UserState"
)
rows = cursor.fetchall()
cursor.close()
@@ -409,6 +416,7 @@ class Database:
"warned": bool(row[6]),
"last_offense_at": float(row[7]) if row[7] is not None else 0.0,
"aliases": row[8] or "",
"warning_expires_at": float(row[9]) if row[9] is not None else 0.0,
}
for row in rows
]