fix: 3 critical autonomous engine & mood system bugs

1. Momentum cliff at 10 messages (P0): The conversation momentum formula
   had a discontinuity where the 10th message caused momentum to DROP from
   0.9 to 0.5. Replaced with a smooth log1p curve that monotonically
   increases (0→0→0.20→0.32→...→0.70→0.89→1.0 at 30 msgs).

2. Neutral keywords overriding all moods (P0): detect_mood_shift() checked
   neutral early with generic keywords (okay, sure, hmm) that matched
   almost any response, constantly resetting mood to neutral. Now: all
   specific moods are scored by match count first (best-match wins),
   neutral is only checked as fallback and requires 2+ keyword matches.

3. Uncancellable delayed_wakeup tasks (P0): Fire-and-forget sleep tasks
   could stack and overwrite mood state after manual wake-up. Added a
   centralized wakeup task registry in ServerManager with automatic
   cancellation on manual wake or new sleep cycle.
This commit is contained in:
2026-02-20 15:37:57 +02:00
parent 2f0d430c35
commit 422366df4c
4 changed files with 97 additions and 46 deletions

View File

@@ -74,6 +74,7 @@ class ServerManager:
self.servers: Dict[int, ServerConfig] = {}
self.schedulers: Dict[int, AsyncIOScheduler] = {}
self.server_memories: Dict[int, Dict] = {}
self._wakeup_tasks: Dict[int, asyncio.Task] = {} # guild_id -> delayed wakeup task
self.load_config()
def load_config(self):
@@ -291,9 +292,70 @@ class ServerManager:
server = self.servers[guild_id]
server.is_sleeping = sleeping
# If waking up, cancel any pending delayed wakeup task
if not sleeping:
self.cancel_wakeup_task(guild_id)
self.save_config()
return True
def schedule_wakeup_task(self, guild_id: int, delay_seconds: int = 3600):
"""Schedule a delayed wakeup task for a server, cancelling any existing one first.
Args:
guild_id: The server to schedule wakeup for
delay_seconds: How long to sleep before waking (default 1 hour)
"""
# Cancel any existing wakeup task for this server
self.cancel_wakeup_task(guild_id)
import globals as _globals
async def _delayed_wakeup():
try:
await asyncio.sleep(delay_seconds)
# Check if we're still asleep (might have been woken manually)
server = self.servers.get(guild_id)
if server and server.is_sleeping:
self.set_server_sleep_state(guild_id, False)
self.set_server_mood(guild_id, "neutral")
# Notify autonomous engine
try:
from utils.autonomous import on_mood_change
on_mood_change(guild_id, "neutral")
except Exception as e:
logger.error(f"Failed to notify autonomous engine of wake-up: {e}")
# Update nickname
try:
from utils.moods import update_server_nickname
await update_server_nickname(guild_id)
except Exception as e:
logger.error(f"Failed to update nickname on wake-up: {e}")
logger.info(f"Server {guild_id} woke up from auto-sleep after {delay_seconds}s")
else:
logger.debug(f"Wakeup task for {guild_id} completed but server already awake, skipping")
except asyncio.CancelledError:
logger.debug(f"Wakeup task for server {guild_id} was cancelled")
finally:
# Clean up our reference
self._wakeup_tasks.pop(guild_id, None)
task = _globals.client.loop.create_task(_delayed_wakeup())
self._wakeup_tasks[guild_id] = task
logger.info(f"Scheduled auto-wake for server {guild_id} in {delay_seconds}s")
return task
def cancel_wakeup_task(self, guild_id: int):
"""Cancel a pending wakeup task for a server, if any."""
task = self._wakeup_tasks.pop(guild_id, None)
if task and not task.done():
task.cancel()
logger.info(f"Cancelled pending wakeup task for server {guild_id}")
def get_server_mood_state(self, guild_id: int) -> dict:
"""Get complete mood state for a specific server"""
if guild_id not in self.servers: