fix: persist last_offense_time and reset offenses after 24h
last_offense_time was in-memory only — lost on restart, so the offense_reset_minutes check never fired after a reboot. Now persisted as LastOffenseAt FLOAT in UserState. On startup hydration, stale offenses (and warned flag) are auto-cleared if the reset window has passed. Bumped offense_reset_minutes from 2h to 24h. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -913,6 +913,7 @@ class SentimentCog(commands.Cog):
|
|||||||
baseline_coherence=user_data.baseline_coherence,
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
user_notes=user_data.notes or None,
|
user_notes=user_data.notes or None,
|
||||||
warned=user_data.warned_since_reset,
|
warned=user_data.warned_since_reset,
|
||||||
|
last_offense_at=user_data.last_offense_time or None,
|
||||||
))
|
))
|
||||||
self._dirty_users.discard(user_id)
|
self._dirty_users.discard(user_id)
|
||||||
|
|
||||||
@@ -940,6 +941,7 @@ class SentimentCog(commands.Cog):
|
|||||||
baseline_coherence=user_data.baseline_coherence,
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
user_notes=user_data.notes or None,
|
user_notes=user_data.notes or None,
|
||||||
warned=user_data.warned_since_reset,
|
warned=user_data.warned_since_reset,
|
||||||
|
last_offense_at=user_data.last_offense_time or None,
|
||||||
)
|
)
|
||||||
logger.info("Flushed %d dirty user states to DB.", len(dirty))
|
logger.info("Flushed %d dirty user states to DB.", len(dirty))
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ mention_scan:
|
|||||||
|
|
||||||
timeouts:
|
timeouts:
|
||||||
escalation_minutes: [30, 60, 120, 240] # Escalating timeout durations
|
escalation_minutes: [30, 60, 120, 240] # Escalating timeout durations
|
||||||
offense_reset_minutes: 120 # Reset offense counter after this much good behavior
|
offense_reset_minutes: 1440 # Reset offense counter after this much good behavior (24h)
|
||||||
warning_cooldown_minutes: 5 # Don't warn same user more than once per this window
|
warning_cooldown_minutes: 5 # Don't warn same user more than once per this window
|
||||||
|
|
||||||
messages:
|
messages:
|
||||||
|
|||||||
+17
-8
@@ -132,6 +132,12 @@ class Database:
|
|||||||
ALTER TABLE UserState ADD Warned BIT NOT NULL DEFAULT 0
|
ALTER TABLE UserState ADD Warned BIT NOT NULL DEFAULT 0
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# --- Schema migration for persisting last offense time ---
|
||||||
|
cursor.execute("""
|
||||||
|
IF COL_LENGTH('UserState', 'LastOffenseAt') IS NULL
|
||||||
|
ALTER TABLE UserState ADD LastOffenseAt FLOAT NULL
|
||||||
|
""")
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
|
||||||
CREATE TABLE BotSettings (
|
CREATE TABLE BotSettings (
|
||||||
@@ -291,19 +297,20 @@ class Database:
|
|||||||
baseline_coherence: float = 0.85,
|
baseline_coherence: float = 0.85,
|
||||||
user_notes: str | None = None,
|
user_notes: str | None = None,
|
||||||
warned: bool = False,
|
warned: bool = False,
|
||||||
|
last_offense_at: float | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned)."""
|
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time)."""
|
||||||
if not self._available:
|
if not self._available:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
self._save_user_state_sync,
|
self._save_user_state_sync,
|
||||||
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned,
|
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to save user state")
|
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):
|
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at):
|
||||||
conn = self._connect()
|
conn = self._connect()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -314,13 +321,14 @@ class Database:
|
|||||||
WHEN MATCHED THEN
|
WHEN MATCHED THEN
|
||||||
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
|
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
|
||||||
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
|
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
|
||||||
|
LastOffenseAt = ?,
|
||||||
UpdatedAt = SYSUTCDATETIME()
|
UpdatedAt = SYSUTCDATETIME()
|
||||||
WHEN NOT MATCHED THEN
|
WHEN NOT MATCHED THEN
|
||||||
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned)
|
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?);""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?);""",
|
||||||
user_id,
|
user_id,
|
||||||
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0,
|
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,
|
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at,
|
||||||
)
|
)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
finally:
|
finally:
|
||||||
@@ -363,7 +371,7 @@ class Database:
|
|||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned FROM UserState"
|
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt FROM UserState"
|
||||||
)
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
@@ -376,6 +384,7 @@ class Database:
|
|||||||
"baseline_coherence": float(row[4]),
|
"baseline_coherence": float(row[4]),
|
||||||
"user_notes": row[5] or "",
|
"user_notes": row[5] or "",
|
||||||
"warned": bool(row[6]),
|
"warned": bool(row[6]),
|
||||||
|
"last_offense_at": float(row[7]) if row[7] is not None else 0.0,
|
||||||
}
|
}
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -286,6 +286,13 @@ class DramaTracker:
|
|||||||
user.notes = state["user_notes"]
|
user.notes = state["user_notes"]
|
||||||
if state.get("warned"):
|
if state.get("warned"):
|
||||||
user.warned_since_reset = True
|
user.warned_since_reset = True
|
||||||
|
if state.get("last_offense_at"):
|
||||||
|
user.last_offense_time = state["last_offense_at"]
|
||||||
|
# Apply time-based offense reset at load time
|
||||||
|
if time.time() - user.last_offense_time > self.offense_reset_seconds:
|
||||||
|
user.offense_count = 0
|
||||||
|
user.warned_since_reset = False
|
||||||
|
user.last_offense_time = 0.0
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user