From 422366df4ce410f2566b069d8944ea250deb7f71 Mon Sep 17 00:00:00 2001 From: koko210Serve Date: Fri, 20 Feb 2026 15:37:57 +0200 Subject: [PATCH] fix: 3 critical autonomous engine & mood system bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- bot/bot.py | 12 ++----- bot/server_manager.py | 62 ++++++++++++++++++++++++++++++++++ bot/utils/autonomous_engine.py | 10 +++--- bot/utils/moods.py | 59 +++++++++++++++----------------- 4 files changed, 97 insertions(+), 46 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 27ce63e..7366207 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -14,6 +14,7 @@ from api import app from config import CONFIG, SECRETS, validate_config, print_config_summary from server_manager import server_manager +from config_manager import config_manager from utils.scheduled import ( send_monday_video ) @@ -106,7 +107,6 @@ async def on_ready(): restore_bipolar_mode_on_startup() # Restore runtime settings (language, debug flags, etc.) from config_runtime.yaml - from config_manager import config_manager config_manager.restore_runtime_settings() # Initialize DM interaction analyzer @@ -709,15 +709,7 @@ async def on_message(message): if detected == "asleep": server_manager.set_server_sleep_state(message.guild.id, True) - # Schedule wake-up after 1 hour - async def delayed_wakeup(): - await asyncio.sleep(3600) # 1 hour - server_manager.set_server_sleep_state(message.guild.id, False) - server_manager.set_server_mood(message.guild.id, "neutral") - await update_server_nickname(message.guild.id) - logger.info(f"🌅 Server {message.guild.name} woke up from auto-sleep") - - globals.client.loop.create_task(delayed_wakeup()) + server_manager.schedule_wakeup_task(message.guild.id, delay_seconds=3600) else: logger.error(f"No server config found for guild {message.guild.id}, skipping mood detection") except Exception as e: diff --git a/bot/server_manager.py b/bot/server_manager.py index eb9c479..a8d9c06 100644 --- a/bot/server_manager.py +++ b/bot/server_manager.py @@ -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: diff --git a/bot/utils/autonomous_engine.py b/bot/utils/autonomous_engine.py index 4cc988b..70d8a5c 100644 --- a/bot/utils/autonomous_engine.py +++ b/bot/utils/autonomous_engine.py @@ -4,6 +4,7 @@ Truly autonomous decision-making engine for Miku. Makes decisions based on context signals without constant LLM polling. """ +import math import time import random from datetime import datetime, timedelta @@ -203,11 +204,12 @@ class AutonomousEngine: ctx.messages_last_hour = sum(1 for t in times if now - t < 3600) # Calculate conversation momentum (0-1 scale) - # High momentum = consistent messages in last 5 minutes - if ctx.messages_last_5min >= 10: - ctx.conversation_momentum = min(1.0, ctx.messages_last_5min / 20) + # Smooth curve: grows quickly at first, then tapers off toward 1.0 + # 1 msg → 0.10, 5 msgs → 0.41, 10 msgs → 0.63, 20 msgs → 0.82, 40 msgs → 0.95 + if ctx.messages_last_5min == 0: + ctx.conversation_momentum = 0.0 else: - ctx.conversation_momentum = ctx.messages_last_5min / 10 + ctx.conversation_momentum = min(1.0, math.log1p(ctx.messages_last_5min) / math.log1p(30)) # Time since last action if guild_id in self.server_last_action: diff --git a/bot/utils/moods.py b/bot/utils/moods.py index a28cc8d..4104f1b 100644 --- a/bot/utils/moods.py +++ b/bot/utils/moods.py @@ -63,10 +63,6 @@ def detect_mood_shift(response_text, server_context=None): "asleep": [ "good night", "goodnight", "sweet dreams", "going to bed", "I will go to bed", "zzz~", "sleep tight" ], - "neutral": [ - "okay", "sure", "alright", "i see", "understood", "hmm", - "sounds good", "makes sense", "alrighty", "fine", "got it" - ], "bubbly": [ "so excited", "feeling bubbly", "super cheerful", "yay!", "✨", "nya~", "kyaa~", "heehee", "bouncy", "so much fun", "i'm glowing!", "nee~", "teehee", "I'm so happy" @@ -116,25 +112,41 @@ def detect_mood_shift(response_text, server_context=None): ] } + # First pass: find ALL matching moods with their match counts (excluding neutral) + response_lower = response_text.lower() + mood_matches = {} for mood, phrases in mood_keywords.items(): - # Check against server mood if provided, otherwise skip if mood == "asleep": + # asleep requires sleepy prerequisite if server_context: - # For server context, check against server's current mood current_mood = server_context.get('current_mood_name', 'neutral') if current_mood != "sleepy": - logger.debug(f"Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'") continue else: - # For DM context, check against DM mood if globals.DM_MOOD != "sleepy": - logger.debug(f"Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'") continue - for phrase in phrases: - if phrase.lower() in response_text.lower(): - logger.info(f"Mood keyword triggered: {phrase}") - return mood + match_count = sum(1 for phrase in phrases if phrase.lower() in response_lower) + if match_count > 0: + mood_matches[mood] = match_count + + if mood_matches: + # Return the mood with the most keyword matches (strongest signal) + best_mood = max(mood_matches, key=mood_matches.get) + logger.info(f"Mood shift detected: {best_mood} ({mood_matches[best_mood]} keyword matches, all matches: {mood_matches})") + return best_mood + + # Neutral is checked separately and only triggers if NOTHING else matched + # Requires 2+ neutral keywords to avoid false positives from casual "okay" / "sure" + neutral_phrases = [ + "okay", "sure", "alright", "i see", "understood", "hmm", + "sounds good", "makes sense", "alrighty", "fine", "got it" + ] + neutral_count = sum(1 for phrase in neutral_phrases if phrase.lower() in response_lower) + if neutral_count >= 2: + logger.info(f"Mood shift detected: neutral ({neutral_count} neutral keywords)") + return "neutral" + return None async def rotate_dm_mood(): @@ -287,27 +299,10 @@ async def rotate_server_mood(guild_id: int): except Exception as mood_notify_error: logger.error(f"Failed to notify autonomous engine of mood change: {mood_notify_error}") - # If transitioning to asleep, set up auto-wake + # If transitioning to asleep, set up auto-wake via centralized registry if new_mood_name == "asleep": server_manager.set_server_sleep_state(guild_id, True) - # Schedule wake-up after 1 hour - async def delayed_wakeup(): - await asyncio.sleep(3600) # 1 hour - server_manager.set_server_sleep_state(guild_id, False) - server_manager.set_server_mood(guild_id, "neutral") - - # V2: Notify autonomous engine of mood change - try: - from utils.autonomous import on_mood_change - on_mood_change(guild_id, "neutral") - except Exception as mood_notify_error: - logger.error(f"Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}") - - await update_server_nickname(guild_id) - logger.info(f"Server {guild_id} woke up from auto-sleep (mood rotation)") - - globals.client.loop.create_task(delayed_wakeup()) - logger.info(f"Scheduled auto-wake for server {guild_id} in 1 hour") + server_manager.schedule_wakeup_task(guild_id, delay_seconds=3600) # Update nickname for this specific server await update_server_nickname(guild_id)