fix(P1): 6 priority-1 bug fixes for autonomous engine and mood system
#4 Sleep/mood desync — set_server_mood() now clears is_sleeping when mood changes away from 'asleep', preventing ghost-sleep state. #5 Race condition in _check_and_act — added per-guild asyncio.Lock so overlapping ticks + message-triggered calls cannot fire concurrently. #6 Class-level attrs on ServerConfig — sleepy_responses_left, angry_wakeup_timer, and forced_angry_until are now proper dataclass fields with defaults, so asdict()/from_dict() round-trip correctly. Also strips unknown keys in from_dict() to survive schema changes. #7 Persistence decay_factor crash — initialise decay_factor = 1.0 before the loop so empty-server or zero-downtime paths don't raise NameError. #8 Double record_action — removed the redundant call in autonomous_tick_v2(); only _check_and_act records the action now. #9 Engine mood desync — on_mood_change() is now called inside set_server_mood() (single source of truth) and removed from 4 call-sites in api.py, moods.py, and server_manager wakeup task.
This commit is contained in:
@@ -18,6 +18,15 @@ logger = get_logger('autonomous')
|
||||
_last_action_execution = {} # guild_id -> timestamp
|
||||
_MIN_ACTION_INTERVAL = 30 # Minimum 30 seconds between autonomous actions
|
||||
|
||||
# Per-guild locks to prevent race conditions from near-simultaneous messages
|
||||
_action_locks: dict = {} # guild_id -> asyncio.Lock
|
||||
|
||||
def _get_action_lock(guild_id: int) -> asyncio.Lock:
|
||||
"""Get or create an asyncio.Lock for a guild."""
|
||||
if guild_id not in _action_locks:
|
||||
_action_locks[guild_id] = asyncio.Lock()
|
||||
return _action_locks[guild_id]
|
||||
|
||||
# Pause state for voice sessions
|
||||
_autonomous_paused = False
|
||||
|
||||
@@ -94,9 +103,6 @@ async def autonomous_tick_v2(guild_id: int):
|
||||
# Record that action was taken
|
||||
autonomous_engine.record_action(guild_id)
|
||||
|
||||
# Record that action was taken
|
||||
autonomous_engine.record_action(guild_id)
|
||||
|
||||
# Update rate limiter
|
||||
_last_action_execution[guild_id] = time.time()
|
||||
|
||||
@@ -201,67 +207,70 @@ async def _check_and_act(guild_id: int):
|
||||
|
||||
IMPORTANT: Pass triggered_by_message=True so the engine knows to respond
|
||||
to the message instead of saying something random/general.
|
||||
|
||||
Uses per-guild lock to prevent race conditions from near-simultaneous messages.
|
||||
"""
|
||||
# Rate limiting check
|
||||
now = time.time()
|
||||
if guild_id in _last_action_execution:
|
||||
time_since_last = now - _last_action_execution[guild_id]
|
||||
if time_since_last < _MIN_ACTION_INTERVAL:
|
||||
return
|
||||
|
||||
action_type = autonomous_engine.should_take_action(guild_id, triggered_by_message=True)
|
||||
|
||||
if action_type:
|
||||
logger.info(f"[V2] Message triggered autonomous action: {action_type}")
|
||||
async with _get_action_lock(guild_id):
|
||||
# Rate limiting check
|
||||
now = time.time()
|
||||
if guild_id in _last_action_execution:
|
||||
time_since_last = now - _last_action_execution[guild_id]
|
||||
if time_since_last < _MIN_ACTION_INTERVAL:
|
||||
return
|
||||
|
||||
# Execute the action directly (don't call autonomous_tick_v2 which would check again)
|
||||
from utils.autonomous_v1_legacy import (
|
||||
miku_say_something_general_for_server,
|
||||
miku_engage_random_user_for_server,
|
||||
share_miku_tweet_for_server,
|
||||
miku_detect_and_join_conversation_for_server
|
||||
)
|
||||
from utils.profile_picture_manager import profile_picture_manager
|
||||
action_type = autonomous_engine.should_take_action(guild_id, triggered_by_message=True)
|
||||
|
||||
try:
|
||||
if action_type == "general":
|
||||
await miku_say_something_general_for_server(guild_id)
|
||||
elif action_type == "engage_user":
|
||||
await miku_engage_random_user_for_server(guild_id)
|
||||
elif action_type == "share_tweet":
|
||||
await share_miku_tweet_for_server(guild_id)
|
||||
elif action_type == "join_conversation":
|
||||
await miku_detect_and_join_conversation_for_server(guild_id)
|
||||
elif action_type == "change_profile_picture":
|
||||
# Get current mood for this server
|
||||
mood, _ = server_manager.get_server_mood(guild_id)
|
||||
logger.info(f"[V2] Changing profile picture (mood: {mood})")
|
||||
result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
|
||||
if result["success"]:
|
||||
logger.info(f"Profile picture changed successfully!")
|
||||
else:
|
||||
logger.warning(f"Profile picture change failed: {result.get('error')}")
|
||||
if action_type:
|
||||
logger.info(f"[V2] Message triggered autonomous action: {action_type}")
|
||||
|
||||
# Record that action was taken
|
||||
autonomous_engine.record_action(guild_id)
|
||||
# Execute the action directly (don't call autonomous_tick_v2 which would check again)
|
||||
from utils.autonomous_v1_legacy import (
|
||||
miku_say_something_general_for_server,
|
||||
miku_engage_random_user_for_server,
|
||||
share_miku_tweet_for_server,
|
||||
miku_detect_and_join_conversation_for_server
|
||||
)
|
||||
from utils.profile_picture_manager import profile_picture_manager
|
||||
|
||||
# Update rate limiter
|
||||
_last_action_execution[guild_id] = time.time()
|
||||
|
||||
# Check for bipolar argument trigger (only if bipolar mode is active)
|
||||
try:
|
||||
from utils.bipolar_mode import maybe_trigger_argument, is_bipolar_mode
|
||||
if is_bipolar_mode():
|
||||
server_config = server_manager.servers.get(guild_id)
|
||||
if server_config and server_config.autonomous_channel_id:
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if channel:
|
||||
await maybe_trigger_argument(channel, globals.client, "Triggered after message-based action")
|
||||
except Exception as bipolar_err:
|
||||
logger.warning(f"Bipolar check error: {bipolar_err}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing message-triggered action: {e}")
|
||||
if action_type == "general":
|
||||
await miku_say_something_general_for_server(guild_id)
|
||||
elif action_type == "engage_user":
|
||||
await miku_engage_random_user_for_server(guild_id)
|
||||
elif action_type == "share_tweet":
|
||||
await share_miku_tweet_for_server(guild_id)
|
||||
elif action_type == "join_conversation":
|
||||
await miku_detect_and_join_conversation_for_server(guild_id)
|
||||
elif action_type == "change_profile_picture":
|
||||
# Get current mood for this server
|
||||
mood, _ = server_manager.get_server_mood(guild_id)
|
||||
logger.info(f"[V2] Changing profile picture (mood: {mood})")
|
||||
result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
|
||||
if result["success"]:
|
||||
logger.info(f"Profile picture changed successfully!")
|
||||
else:
|
||||
logger.warning(f"Profile picture change failed: {result.get('error')}")
|
||||
|
||||
# Record that action was taken
|
||||
autonomous_engine.record_action(guild_id)
|
||||
|
||||
# Update rate limiter
|
||||
_last_action_execution[guild_id] = time.time()
|
||||
|
||||
# Check for bipolar argument trigger (only if bipolar mode is active)
|
||||
try:
|
||||
from utils.bipolar_mode import maybe_trigger_argument, is_bipolar_mode
|
||||
if is_bipolar_mode():
|
||||
server_config = server_manager.servers.get(guild_id)
|
||||
if server_config and server_config.autonomous_channel_id:
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if channel:
|
||||
await maybe_trigger_argument(channel, globals.client, "Triggered after message-based action")
|
||||
except Exception as bipolar_err:
|
||||
logger.warning(f"Bipolar check error: {bipolar_err}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing message-triggered action: {e}")
|
||||
|
||||
|
||||
def on_presence_update(member, before, after):
|
||||
|
||||
Reference in New Issue
Block a user