fix(tasks): replace fire-and-forget asyncio.create_task with create_tracked_task

Add utils/task_tracker.py with create_tracked_task() that wraps background
tasks with error logging, cancellation handling, and reference tracking.

Replace all 17 fire-and-forget asyncio.create_task() calls across 7 files:
- bot/bot.py (5 interjection checks)
- bot/utils/autonomous.py (2 check-and-act/react tasks)
- bot/utils/bipolar_mode.py (3 argument tasks)
- bot/commands/uno.py (1 game loop task)
- bot/utils/voice_receiver.py (3 STT/interruption callbacks)
- bot/utils/persona_dialogue.py (4 dialogue turn/interjection tasks)

Previously-tracked tasks (voice_audio.py, voice_manager.py) were left as-is
since they already store task references for cancellation.

Closes #1
This commit is contained in:
2026-02-18 12:01:08 +02:00
parent cf55b15745
commit 7b7abcfc68
7 changed files with 104 additions and 25 deletions

54
bot/utils/task_tracker.py Normal file
View File

@@ -0,0 +1,54 @@
# utils/task_tracker.py
"""
Tracked asyncio task creation utility.
Replaces fire-and-forget asyncio.create_task() calls with error-logging wrappers
so that exceptions in background tasks are never silently swallowed.
"""
import asyncio
from typing import Optional, Coroutine, Set
from utils.logger import get_logger
logger = get_logger("task_tracker")
# Keep references to running tasks so they aren't garbage-collected
_active_tasks: Set[asyncio.Task] = set()
def create_tracked_task(
coro: Coroutine,
task_name: Optional[str] = None,
) -> asyncio.Task:
"""
Create an asyncio task with automatic error logging.
Unlike bare asyncio.create_task(), this wrapper:
- Names the task for easier debugging
- Logs any unhandled exception (with full traceback) instead of swallowing it
- Keeps a strong reference so the task isn't garbage-collected mid-flight
- Auto-cleans the reference set when the task finishes
Args:
coro: The coroutine to schedule.
task_name: Human-readable name for log messages.
Defaults to the coroutine's __qualname__.
Returns:
The created asyncio.Task (tracked internally).
"""
name = task_name or getattr(coro, "__qualname__", str(coro))
async def _wrapped():
try:
await coro
except asyncio.CancelledError:
logger.debug(f"Task '{name}' was cancelled")
raise # re-raise so Task.cancelled() works correctly
except Exception:
logger.error(f"Background task '{name}' failed", exc_info=True)
task = asyncio.create_task(_wrapped(), name=name)
_active_tasks.add(task)
task.add_done_callback(_active_tasks.discard)
return task