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:
12
bot/bot.py
12
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user