     1→"""Configuration synchronization manager for database-to-file syncing.
     2→
     3→With Redis enabled, configs are cached in Redis so only ONE pod needs to
     4→hit the DB per refresh cycle. Other pods read from Redis.
     5→
     6→Flow per config:
     7→  1. Try Redis → if fresh, load into in-memory global (skip DB)
     8→  2. If Redis miss → fetch from DB (updates global), then cache to Redis
     9→"""
    10→
    11→import os
    12→import asyncio
    13→from typing import Any, Callable, Awaitable, Optional
    14→from loguru import logger
    15→
    16→from utils import turso_client
    17→from utils.redis_client import redis_available
    18→from utils.cache_layer import CacheKeys
    19→
    20→
    21→class ConfigSyncManager:
    22→    def __init__(self, sync_interval: int = 300):
    23→        self._sync_interval = sync_interval
    24→        self._tasks: list = []
    25→        self._running = False
    26→
    27→    async def start(self):
    28→        """Start all sync tasks."""
    29→        if self._running:
    30→            return
    31→
    32→        self._running = True
    33→        self._tasks = [
    34→            asyncio.create_task(self._sync_prompt()),
    35→            asyncio.create_task(self._sync_images_config()),
    36→            asyncio.create_task(self._sync_subscription_referral()),
    37→            asyncio.create_task(self._sync_gcp_config()),
    38→            asyncio.create_task(self._sync_stt_config()),
    39→            asyncio.create_task(self._sync_llm_provider()),
    40→            asyncio.create_task(self._sync_prompt_writer_config()),
    41→            asyncio.create_task(self._sync_image_describer_config()),
    42→            asyncio.create_task(self._sync_azure_edit_config()),
    43→            asyncio.create_task(self._sync_video_config()),
    44→            asyncio.create_task(self._sync_explore_cache()),
    45→            asyncio.create_task(self._sync_video_showcase()),
    46→            asyncio.create_task(self._sync_cf_flux_config()),
    47→        ]
    48→        logger.info(f"ConfigSyncManager started (redis={'enabled' if redis_available() else 'disabled'})")
    49→
    50→    async def stop(self):
    51→        """Stop all sync tasks."""
    52→        self._running = False
    53→        for task in self._tasks:
    54→            if task and not task.done():
    55→                task.cancel()
    56→                try:
    57→                    await task
    58→                except asyncio.CancelledError:
    59→                    pass
    60→        self._tasks = []
    61→        logger.info("ConfigSyncManager stopped")
    62→
    63→    # ------------------------------------------------------------------
    64→    # Generic Redis-backed config sync helper
    65→    # ------------------------------------------------------------------
    66→    async def _redis_cached_refresh(
    67→        self,
    68→        redis_key: str,
    69→        db_refresh_fn: Callable[[], Awaitable[None]],
    70→        get_cached_fn: Callable[[], Any],
    71→        set_cached_fn: Callable[[Any], None],
    72→        ttl: int = 300,
    73→    ):
    74→        """Try Redis first, fallback to DB refresh.
    75→
    76→        Args:
    77→            redis_key: Redis key for this config.
    78→            db_refresh_fn: Async function that fetches from DB and sets in-memory global.
    79→            get_cached_fn: Function that returns the current in-memory global value.
    80→            set_cached_fn: Function that sets the in-memory global from cached data.
    81→            ttl: Redis TTL in seconds.
    82→        """
    83→        from utils.redis_client import cache_get_json, cache_set_json
    84→
    85→        if redis_available():
    86→            try:
    87→                cached = await cache_get_json(redis_key)
    88→                if cached is not None:
    89→                    set_cached_fn(cached)
    90→                    logger.debug(f"CONFIG_FROM_REDIS: key={redis_key}")
    91→                    return
    92→            except Exception as e:
    93→                logger.debug(f"REDIS_CONFIG_READ_ERROR: key={redis_key} error={e}")
    94→
    95→        # Redis miss or unavailable → hit DB
    96→        await db_refresh_fn()
    97→
    98→        # After DB refresh, cache to Redis
    99→        if redis_available():
   100→            try:


     1→import asyncio
     2→import base64
     3→from datetime import datetime
     4→from loguru import logger
     5→
     6→from pipecat.frames.frames import InterruptionFrame, LLMMessagesAppendFrame
     7→from utils.s3_utils import upload_to_s3
     8→from fnHandlers.utils.image_describer import describe_image_safe
     9→
    10→from typing import TYPE_CHECKING, List
    11→if TYPE_CHECKING:
    12→    from bot.config import BotSessionState, TranscriptManager
    13→
    14→import httpx
    15→
    16→
    17→async def handle_load_image_from_url(
    18→    s3_url: str,
    19→    bot_session: "BotSessionState",
    20→    rtvi,
    21→    task,
    22→    image_id: str = None
    23→):
    24→    """Load an image from S3/CloudFront URL for editing (from history)."""
    25→    session_id = bot_session.session_id or 'unknown'
    26→    device_id = bot_session.device_id or 'common'
    27→
    28→    logger.info(f"LOAD_IMAGE_FROM_URL_STARTED: session_id={session_id} s3_url={s3_url} image_id={image_id}")
    29→
    30→    try:
    31→        # Send loading status
    32→        logger.info(f"LOAD_RTVI_SENDING_LOADING: session_id={session_id} s3_url={s3_url[:80]}...")
    33→
    34→        await rtvi.send_server_message({
    35→            "type": "image_loaded_for_edit",
    36→            "data": {"status": "loading", "s3_url": s3_url}
    37→        })
    38→
    39→        logger.info(f"LOAD_RTVI_LOADING_SENT: session_id={session_id}")
    40→    except Exception as e:
    41→        logger.error(f"LOAD_RTVI_LOADING_FAILED: session_id={session_id} error={e}")
    42→
    43→    try:
    44→        # Download image from URL
    45→        async with httpx.AsyncClient(timeout=30.0) as client:
    46→            response = await client.get(s3_url)
    47→            response.raise_for_status()
    48→            image_bytes = response.content
    49→
    50→        # Convert to base64
    51→        image_b64 = base64.b64encode(image_bytes).decode('utf-8')
    52→
    53→        # Detect mime type from URL
    54→        mime_type = "image/png" if ".png" in s3_url.lower() else "image/jpeg"
    55→
    56→        # Store for editing
    57→        image_info = {
    58→            "data": image_b64,
    59→            "mime_type": mime_type,
    60→            "s3_url": s3_url,
    61→            "source": "history_load",
    62→            "image_id": image_id,
    63→            "loaded_at": datetime.now().isoformat()
    64→        }
    65→
    66→        bot_session.set_uploaded_image(image_info)
    67→
    68→        logger.info(f"LOAD_IMAGE_FROM_URL_SUCCESS: session_id={session_id} size_bytes={len(image_bytes)} s3_url={s3_url}")
    69→
    70→        # Send success response
    71→        success_data = {
    72→            "status": "loaded",
    73→            "image_id": image_id,
    74→            "s3_url": s3_url,
    75→            "mime_type": mime_type,
    76→            "size_bytes": len(image_bytes),
    77→            "ready_for_edit": True
    78→        }
    79→
    80→        logger.info(f"LOAD_RTVI_SENDING_SUCCESS: session_id={session_id} image_id={image_id} size_bytes={len(image_bytes)}")
    81→
    82→        await rtvi.send_server_message({
    83→            "type": "image_loaded_for_edit",
    84→            "data": success_data
    85→        })
    86→
    87→        logger.info(f"LOAD_RTVI_SUCCESS_SENT: session_id={session_id}")
    88→
    89→        # Notify LLM
    90→        await task.queue_frames([InterruptionFrame()])
    91→        await asyncio.sleep(0.1)
    92→        await task.queue_frames([
    93→            LLMMessagesAppendFrame(
    94→                messages=[{"role": "user", "content": "[System: User loaded a previous image from history for editing. It's now ready. Acknowledge briefly and ask what edits they want to make.]"}],
    95→                run_llm=True
    96→            )
    97→        ])
    98→        logger.info(f"LOAD_IMAGE_INTERRUPTION: session_id={session_id}")
    99→
   100→    except Exception as e:


Now let me compile all the findings into a comprehensive report. Let me search for a few more specific patterns:


Bash

find /home/ubuntu/pipecat -type f -name "*.py" | wc -l

Count total Python files


grep -r "\.append\(\|\.extend\(\|\.update\(" /home/ubuntu/pipecat --include="*.py" | grep -E "_(cache|config|tasks|queue)" | head -30

Find appends to cache-related variables


grep: Unmatched ( or \(


Let me check one more potential issue area:


Found 5 files
utils/redis_client.py
utils/s3_utils.py
utils/custom_tts.py
services/mistral_stt.py
pipecat_flow_example.py


Perfect! Now I have enough information to compile a comprehensive report. Let me create the final analysis:

## Comprehensive Memory Leak Analysis Report for Pipecat Codebase

I've conducted a thorough search of the pipecat codebase at `/home/ubuntu/pipecat` for potential memory leak patterns. Below is a detailed report of all identified issues, categorized by type.

---

### 1. GLOBAL CACHING PATTERNS - UNBOUNDED GROWTH RISK

#### Issue 1.1: Character Prompts Cache (No Max Size)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 2944, 3033, 3036, 3040, 3066, 3078
- **Pattern**:
```python
_character_prompts_cache: Dict[str, str] = {}
_prompt_cache_time: Dict[str, float] = {}
```
- **Problem**: Dictionary grows unboundedly as new character IDs are added. No maximum size, no LRU eviction.
- **Accumulation Risk**: Each character prompt cached indefinitely. With many character IDs, this can consume significant memory.
- **No TTL Enforcement**: Uses `CHARACTERS_CACHE_TTL_SECONDS` for validation but caches are never cleared—old entries persist.

#### Issue 1.2: Characters List Cache (Stale Cache Risk)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 2940-2941, 3083-3090, 3160-3161
- **Pattern**:
```python
_characters_cache: Optional[List[Dict[str, Any]]] = None
_characters_cache_time: float = 0
```
- **Problem**: Single shared cache for all characters. When refreshed, entire list replaced but old list not explicitly deleted.
- **Cache Age Check Issue** (Line 3088): Cache validity checked but cache is never actively cleared—relies on Python GC.

#### Issue 1.3: Explore Cache (Dictionary with Lists)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 2313-2397, 2523-2525
- **Pattern**:
```python
_explore_cache = {
    "categories": [],
    "items_by_category": {},
    "all_items": []
}
```
- **Problem**: Lists inside dictionary grow on every refresh. Lists are replaced but old ones aren't explicitly cleaned.
- **Line 2393**: `items_by_category[cat_id].append(item)` - list grows without bounds per category.

#### Issue 1.4: Video Config Cache (Global Dict)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 2874, 2883, 2906
- **Pattern**:
```python
_video_config_cache = { ... }
# Line 2906: _video_config_cache[key] = value.lower() == 'true'
```
- **Problem**: Dictionary keys continuously added without cleanup or max size limits.

#### Issue 1.5: CF Flux Config Cache (Global)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 3631, 3643, 3706-3708
- **Problem**: Optional global config stored at module level. No mechanism to clear or limit size.

#### Issue 1.6: Images Count Config (Global State)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 716, 720, 760, 777-778
- **Problem**: `_images_count_config` global is updated but never cleared. Reference maintained indefinitely.

#### Issue 1.7: Subscription & Referral Config (Module-Level Globals)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 1425-1426, 1430, 1480, 1533, 1542
- **Pattern**:
```python
_subscription_config: Optional[Dict[str, Any]] = None
_referral_config: Optional[Dict[str, Any]] = None
```
- **Problem**: Persistent module-level state updated but never cleared.

#### Issue 1.8: Additional Module-Level Caches (Multiple)
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 1654-1657, 1939, 2033, 2100, 2130, 2325, 2581
- **Caches**:
  - `_gcp_config` (line 1654)
  - `_stt_config` (line 1655)
  - `_prompt_writer_config` (line 1656)
  - `_image_describer_config` (line 1657)
  - `_azure_edit_enabled` (line 2130)
  - `_video_showcase_cache` (line 2581)
- **Problem**: All are module-level globals with no cleanup strategy.

---

### 2. ACTIVE SESSION TRACKING - DICT ACCUMULATION RISK

#### Issue 2.1: Active Sessions Dictionary
**File**: `/home/ubuntu/pipecat/routes/bot.py`
- **Lines**: 21, 86, 95, 113
- **Pattern**:
```python
active_sessions: dict[asyncio.Task, str] = {}
# Line 86: active_sessions[task] = room_name
# Line 95: room = active_sessions.pop(t, None)  # Cleanup via callback
```
- **Problem**: While cleanup happens in `cleanup_session` callback (line 94-112), the callback is only invoked when task completes. If task gets orphaned or callback fails to execute, entries remain.
- **Callback Risk** (Lines 100-110): Async cleanup scheduled via `asyncio.create_task(remove_active_session(room))` inside callback. If event loop is shutting down, cleanup skips Redis removal.
- **Missing Guarantees**: No explicit cleanup if callback fails or is never called.

#### Issue 2.2: Participants Dictionary in BotSessionState
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 137, 186-190, 531
- **Pattern**:
```python
self.participants: dict = {}
# Line 186: self.participants[participant_id] = { ... }
# Line 531: bot_session.participants.clear()  # Only cleared at session end
```
- **Problem**: Grows throughout session as participants join. Cleared only at session cleanup. If cleanup fails or is delayed, participants dict continues accumulating.

---

### 3. ASYNC TASK MANAGEMENT - UNTRACKED FIRE-AND-FORGET TASKS

#### Issue 3.1: ConfigSyncManager Background Tasks
**File**: `/home/ubuntu/pipecat/utils/config_sync.py`
- **Lines**: 33-47, 50-61
- **Pattern**:
```python
self._tasks = [
    asyncio.create_task(self._sync_prompt()),
    asyncio.create_task(self._sync_images_config()),
    ... (13 tasks created)
]
```
- **Problem**: 13 background tasks created at startup without individual error handling. If one task fails silently, it's not immediately obvious.
- **Stop Method** (Line 50-61): Tasks are cancelled on shutdown, but if `stop()` never called, all tasks run forever accumulating state.

#### Issue 3.2: Fire-and-Forget Background Tasks in Session
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 323-332
- **Pattern**:
```python
def track_background_task(self, task: asyncio.Task):
    self._background_tasks.append(task)
    def _on_task_done(t):
        try:
            self._background_tasks.remove(t)
        except ValueError:
            pass
    task.add_done_callback(_on_task_done)
```
- **Problem**: Auto-removal via callback (line 329). But if callback fails, task remains in list. Over long sessions with many tasks, this accumulates.
- **Used By** (lines 256, 302): `check_review_eligibility_background()` and `send_session_info()` tracked here.

#### Issue 3.3: Untracked asyncio.create_task in Session Handlers
**File**: `/home/ubuntu/pipecat/bot/session_handlers.py`
- **Lines**: 256, 302
- **Pattern**:
```python
bot_session.track_background_task(asyncio.create_task(check_review_eligibility_background()))
bot_session.track_background_task(asyncio.create_task(send_session_info()))
```
- **Problem**: Properly tracked via `track_background_task()`, but tracking itself relies on done callbacks.

#### Issue 3.4: Untracked Task in Session.state_processor
**File**: `/home/ubuntu/pipecat/utils/session_state.py`
- **Lines**: 43, 46
- **Pattern**:
```python
self.image_processor_task = asyncio.create_task(self._process_image_queue())
self.audio_processor_task = asyncio.create_task(self._process_audio_queue())
```
- **Problem**: Tasks stored as instance attributes, but cleanup (line 50-57) only happens on `stop_queue_processor()` call. If not called, tasks run indefinitely.

#### Issue 3.5: Mistral STT Background Tasks
**File**: `/home/ubuntu/pipecat/services/mistral_stt.py`
- **Lines**: 205-206, 215-241
- **Pattern**:
```python
self._send_task = asyncio.create_task(self._send_audio_loop())
self._receive_task = asyncio.create_task(self._receive_transcription_loop())
# Cleanup: lines 221-230
```
- **Problem**: Tasks properly cancelled in `_disconnect()` (lines 221-230), but only if disconnect is called. If service is abandoned without cleanup, tasks remain active.

#### Issue 3.6: Video Generation Background Tasks
**File**: `/home/ubuntu/pipecat/fnHandlers/video_handler.py`
- **Lines**: 282, 543
- **Pattern**:
```python
asyncio.create_task(generate_video_background())
asyncio.create_task(generate_i2v_background())
```
- **Problem**: Fire-and-forget tasks with no tracking. If generation fails silently, task is lost.

---

### 4. HTTP/WEBSOCKET CONNECTION POOLS - SHARED GLOBAL STATE

#### Issue 4.1: Daily Client Shared aiohttp Session
**File**: `/home/ubuntu/pipecat/utils/daily_client.py`
- **Lines**: 12, 15-22
- **Pattern**:
```python
_shared_session: Optional[aiohttp.ClientSession] = None

async def _get_session() -> aiohttp.ClientSession:
    global _shared_session
    if _shared_session is None or _shared_session.closed:
        _shared_session = httpx.AsyncClient(...)
    return _shared_session
```
- **Problem**: Shared session never explicitly closed. If closed, a new one is created but old one reference may remain.
- **No Cleanup Method**: No way to gracefully shutdown or clear this session.

#### Issue 4.2: CF Flux Client Shared httpx Connection
**File**: `/home/ubuntu/pipecat/fnHandlers/clients/cf_flux_client.py`
- **Lines**: 17, 25-31
- **Pattern**:
```python
_shared_client: Optional[httpx.AsyncClient] = None
_rr_counter = 0  # Round-robin counter (mutable global!)

def _get_client(timeout: float = 120.0) -> httpx.AsyncClient:
    global _shared_client
    if _shared_client is None or _shared_client.is_closed:
        _shared_client = httpx.AsyncClient(...)
    return _shared_client
```
- **Problem**: Shared client never closed. Closed clients recreated but old ones don't get garbage collected deterministically.
- **Round-Robin Counter** (line 20, 59-63): `_rr_counter` is a mutable global that increments forever. No reset or limit.

#### Issue 4.3: Edit Service Client Shared httpx Connection
**File**: `/home/ubuntu/pipecat/fnHandlers/clients/edit_service_client.py`
- **Lines**: 13, 18-24
- **Pattern**:
```python
_shared_client: Optional[httpx.AsyncClient] = None

def _get_client(timeout: float = 120.0) -> httpx.AsyncClient:
    global _shared_client
    if _shared_client is None or _shared_client.is_closed:
        _shared_client = httpx.AsyncClient(...)
    return _shared_client
```
- **Problem**: Identical to CF Flux client—shared session never explicitly closed.

#### Issue 4.4: Custom TTS HTTP Session
**File**: `/home/ubuntu/pipecat/utils/custom_tts.py`
- **Lines**: 36-37, 45-68, 61-68
- **Pattern**:
```python
self._session = None
self._connector = None

async def _ensure_session(self):
    if self._session is None:
        self._connector = aiohttp.TCPConnector(...)
        self._session = aiohttp.ClientSession(connector=self._connector, ...)

async def close(self):
    if self._session:
        await self._session.close()
        self._session = None
    if self._connector:
        await self._connector.close()
```
- **Problem**: Properly closed in `close()` method, but if close() never called, connector and session persist.
- **Missing Cleanup Guarantee**: No `__del__` or context manager to ensure cleanup.

---

### 5. WEBSOCKET CONNECTIONS - IMPROPER CLEANUP

#### Issue 5.1: Mistral STT WebSocket
**File**: `/home/ubuntu/pipecat/services/mistral_stt.py`
- **Lines**: 92, 176, 205-206, 215-241, 273
- **Pattern**:
```python
self._websocket = None
# Connect: line 176: self._websocket = await websockets.connect(self._ws_url)
# Cleanup: lines 233-238
if self._websocket:
    try:
        await self._websocket.close()
    except Exception:
        pass
    self._websocket = None
```
- **Problem**: WebSocket properly closed, but only if `_disconnect()` is called. If disconnect never invoked (e.g., due to exception), WebSocket remains open.
- **Message Loop** (line 273): `async for message in self._websocket` will hang if WebSocket never closed and no more messages arrive.

---

### 6. FILE HANDLES - LOGGING & CONFIGURATION

#### Issue 6.1: Datadog File Sink - Open File Handle
**File**: `/home/ubuntu/pipecat/utils/logging_config.py`
- **Lines**: 126-144
- **Pattern**:
```python
class DatadogFileSink:
    def __init__(self, filepath):
        self._file = None

    def _ensure_file(self):
        if self._file is None:
            self._file = open(self.filepath, "a", encoding="utf-8")

    def close(self):
        if self._file:
            self._file.close()
```
- **Problem**: File opened lazily but `close()` method exists. However, sink added to logger (lines 274-290) and close() is never called on sink during shutdown.
- **No Context Manager**: No guarantee file handle is closed on app shutdown.

#### Issue 6.2: Log File - Long-Lived Handle
**File**: `/home/ubuntu/pipecat/utils/logging_config.py`
- **Lines**: 243-254
- **Pattern**:
```python
logger.add(
    constants.LOG_FILE_PATH,
    level="DEBUG",
    rotation=constants.LOG_ROTATION_SIZE,
    retention=constants.LOG_RETENTION,
    compression="zip",
    ...
)
```
- **Problem**: Loguru file handler added at startup but never explicitly removed. File handle remains open for lifetime of process.
- **Log Rotation**: While rotation exists, retention doesn't guarantee cleanup of old logs quickly.

---

### 7. IMAGE DATA IN MEMORY - LARGE BASE64 STRINGS

#### Issue 7.1: Image Batch Storage in BotSessionState
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 147-148, 250-272, 303-308
- **Pattern**:
```python
self.image_batch: list = []  # List of image dicts
# Line 256: self.image_batch = images[:self.max_batch_size]  # Limit to max
# Line 269-271: def clear_image_batch(self): self.image_batch = []
```
- **Problem**: Each image dict contains base64-encoded image data (2-10MB per image). List cleared at session end, but if session cleanup fails or is delayed, images persist in memory.
- **Backward Compatibility Issue** (line 259): `self.uploaded_image` also stores full image data.

#### Issue 7.2: Generated/Edited Images Storage
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 144-146, 158, 287-289
- **Pattern**:
```python
self.uploaded_image: Optional[dict] = None
self.last_image: Optional[dict] = None
self.last_generated_image: Optional[dict] = None
self.generated_videos: List[Dict] = []
```
- **Problem**: Each stores full base64 image/video data. Multiple references mean images must be manually cleared in all 4 locations during cleanup.
- **Partial Cleanup Risk** (line 303-308): `clear_all_images()` clears all, but if only partial clear happens, stale references persist.

#### Issue 7.3: Uploaded Image Queue in SessionState
**File**: `/home/ubuntu/pipecat/utils/session_state.py`
- **Lines**: 30, 34, 60-61, 63-73
- **Pattern**:
```python
self.image_queue = asyncio.Queue()
self.audio_queue = asyncio.Queue()
# Drain on stop (lines 60-61)
self._drain_queue(self.image_queue)
```
- **Problem**: Queues drained on stop, but items in queue are never garbage collected until queue is fully emptied. If `_drain_queue()` fails, items remain.

---

### 8. CONTEXT AGGREGATOR - UNBOUNDED GROWTH

#### Issue 8.1: LLM Context Aggregator
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 165, 451-453
- **Pattern**:
```python
self.context_aggregator = None  # LLM context (grows with conversation)
# ...
if bot_session.context_aggregator:
    bot_session.context_aggregator = None
    logger.debug(f"CLEANUP: context aggregator released session_id={session_id}")
```
- **Problem**: Context aggregator reference set to None but actual object cleanup depends on Python GC. For long conversations, this can accumulate GBs.
- **No Explicit Clear**: Should call `context_aggregator.clear()` before setting to None.

---

### 9. PIPELINE & TRANSPORT CLEANUP - DEPENDENCY ORDER RISK

#### Issue 9.1: Pipeline Cleanup Sequence
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 457-503
- **Pattern**:
```python
# 7. Cleanup pipeline first
pipeline_cleanup_success = False
try:
    if bot_session.pipeline:
        if hasattr(bot_session.pipeline, 'cleanup'):
            await bot_session.pipeline.cleanup()
            pipeline_cleanup_success = True
        bot_session.pipeline = None
# 8. Cleanup transport (only if pipeline cleanup failed)
if bot_session.transport:
    if not pipeline_cleanup_success:
        if hasattr(bot_session.transport, 'cleanup'):
            await bot_session.transport.cleanup()
```
- **Problem**: Transport cleanup skipped if pipeline cleanup succeeds. But if `hasattr()` check returns True but `cleanup()` throws, `pipeline_cleanup_success` stays False and transport cleanup still runs.
- **Double Cleanup Risk**: If pipeline.cleanup() calls transport.cleanup() internally and also succeeds, transport cleanup is skipped (correct). But if pipeline is None or doesn't have cleanup method, transport cleanup never happens.
- **Exception Handling** (line 471-477): Exceptions in pipeline cleanup are caught but pipeline reference is still set to None, preventing retry.

#### Issue 9.2: Transport Reference Lingering
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 163, 485-503
- **Pattern**:
```python
self.transport = None  # Set at init
# ...
if bot_session.transport:
    if not pipeline_cleanup_success:
        # Only cleanup if pipeline didn't
```
- **Problem**: Transport only cleaned up if pipeline cleanup failed. This is intentional to avoid double-cleanup, but creates risk: if pipeline cleanup was incomplete (exception caught), transport might leak.

---

### 10. VAD ANALYZER - NATIVE RESOURCE LEAK

#### Issue 10.1: ONNX VAD Model Not Cleaned
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 505-513
- **Pattern**:
```python
if bot_session.vad_analyzer:
    try:
        del bot_session.vad_analyzer
        logger.info(f"VAD_DESTROYED: session_id={session_id} (memory freed)")
    except Exception as e:
        logger.error(f"CLEANUP_ERROR: VAD destruction failed: {e}")
    finally:
        bot_session.vad_analyzer = None
```
- **Problem**: Just deletes reference via `del`, doesn't call cleanup method. ONNX models may have cleanup hooks (`__del__`, `.cleanup()`, etc.) that are not invoked.
- **No Explicit Cleanup Call**: Should check `hasattr(vad_analyzer, 'cleanup')` and call it before delete.

---

### 11. UNCLEARED BUFFERS & AGGREGATORS

#### Issue 11.1: Audio Buffer in BotSessionState
**File**: `/home/ubuntu/pipecat/bot/config.py`
- **Lines**: 142, 524-527
- **Pattern**:
```python
self.audio_buffer = None  # At init
# ...
if bot_session.audio_buffer:
    bot_session.audio_buffer = None
```
- **Problem**: Set to None without calling any cleanup method on the audio buffer object. If buffer has internal state, it's not cleaned.

---

### 12. REDIS CONNECTION POOL - INITIALIZATION RACE

#### Issue 12.1: Redis Pool Global State
**File**: `/home/ubuntu/pipecat/utils/redis_client.py`
- **Lines**: 27-29, 32-69, 77-79
- **Pattern**:
```python
_redis_pool = None
_redis_available = False
_init_attempted = False

async def init_redis() -> bool:
    global _redis_pool, _redis_available, _init_attempted
    if _init_attempted:
        return _redis_available
    _init_attempted = True
    # ... create _redis_pool
```
- **Problem**: Once `_init_attempted = True`, pool creation is never retried even if pool becomes invalid later. If Redis disconnects and reconnects, pool handle is stale.
- **No Cleanup Method**: No way to reset `_init_attempted` or recreate pool without restart.

---

### 13. TURSO DATABASE CLIENT - SINGLETON PATTERN WITH NO CLEANUP

#### Issue 13.1: HTTP Session in TursoHTTPClient
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 95-111
- **Pattern**:
```python
class TursoHTTPClient:
    def __init__(self, url: str, token: str):
        self.session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(
            pool_connections=constants.HTTP_POOL_CONNECTIONS,
            pool_maxsize=constants.HTTP_POOL_MAXSIZE,
            pool_block=False
        )
        self.session.mount('https://', adapter)
```
- **Problem**: Session created but never closed. No `close()` method or context manager.
- **Singleton Pattern** (line 194, 198): `_turso_client` is a module-level singleton never destroyed.
- **Request Pooling**: HTTPAdapter pool has max size but connection pool never explicitly closed.

#### Issue 13.2: Turso Client Singleton Lifecycle
**File**: `/home/ubuntu/pipecat/utils/turso_client.py`
- **Lines**: 194, 198-210
- **Pattern**:
```python
_turso_client: Optional[TursoHTTPClient] = None

def get_turso_client():
    global _turso_client
    if _turso_client is None:
        _turso_client = TursoHTTPClient(
            constants.TURSO_DATABASE_URL,
            constants.TURSO_AUTH_TOKEN
        )
    return _turso_client
```
- **Problem**: Once created, never destroyed. No cleanup on app shutdown.

---

### 14. EVENT HANDLER REGISTRATION - POTENTIAL CALLBACK ACCUMULATION

#### Issue 14.1: Transport Event Handlers
**File**: `/home/ubuntu/pipecat/bot/main.py`
- **Lines**: 111, 155-165
- **Pattern**:
```python
audio_buffer.event_handler("on_audio_data")(create_audio_data_handler(...))
transcript.event_handler("on_transcript_update")(create_transcript_handler(...))
transport.event_handler("on_participant_joined")(create_participant_joined_handler(...))
transport.event_handler("on_client_disconnected")(create_client_disconnected_handler(...))
task.event_handler("on_idle_timeout")(create_idle_timeout_handler(...))
```
- **Problem**: Multiple event handlers registered per session. If handler is not unregistered on cleanup, they accumulate.
- **Closure Captures**: Each handler is a closure capturing session state, task, etc. If not garbage collected, these references prevent memory release.
- **No Deregistration**: No evidence of calling `.remove_handler()` or similar on shutdown.

---

### 15. LONG-LIVED LOGGING SETUP - FILE SINK LEAK

#### Issue 15.1: Logger Handlers Not Cleaned
**File**: `/home/ubuntu/pipecat/utils/logging_config.py`
- **Lines**: 224-254, 274-290
- **Pattern**:
```python
json_stdout_sink = DatadogStdoutSink()
logger.add(
    json_stdout_sink.write,
    level=stdout_level,
    filter=log_filter,
)

sink = DatadogFileSink(datadog_path)
logger.add(
    sink.write,
    level="INFO",
    filter=log_filter,
)
```
- **Problem**: Sinks added via `logger.add()` but sink objects are not stored for later cleanup. Calling `sink.close()` won't work because references are lost.
- **No Removal on Shutdown**: No `logger.remove()` calls on app shutdown to close file handles.

---

### 16. BACKGROUND SYNC TASKS - PERMANENT MEMORY GROWTH

#### Issue 16.1: ConfigSyncManager Startup
**File**: `/home/ubuntu/pipecat/utils/config_sync.py`
- **Lines**: 33-47, 48
- **Pattern**:
```python
self._tasks = [
    asyncio.create_task(self._sync_prompt()),
    asyncio.create_task(self._sync_images_config()),
    ... 13 total tasks
]
logger.info(f"ConfigSyncManager started (redis={'enabled' if redis_available() else 'disabled'})")
```
- **Problem**: All 13 sync tasks run in infinite loops (checking inside each task). They continuously fetch config and update module-level globals.
- **No Limit on Task List**: Each sync task manages its own state but if any exception occurs, task memory persists.

---

### SUMMARY TABLE

| Category | Severity | Count | Files Affected |
|----------|----------|-------|-----------------|
| Unbounded Global Caches | HIGH | 8 | turso_client.py |
| Async Task Tracking | HIGH | 6 | config_sync.py, config.py, session_state.py, mistral_stt.py, video_handler.py |
| HTTP/WebSocket Pools | HIGH | 4 | daily_client.py, cf_flux_client.py, edit_service_client.py, custom_tts.py |
| WebSocket Cleanup | MEDIUM | 1 | mistral_stt.py |
| File Handles | MEDIUM | 2 | logging_config.py |
| Image Data in Memory | HIGH | 3 | config.py, session_state.py |
| Context Aggregator | MEDIUM | 1 | config.py |
| Pipeline/Transport Cleanup | MEDIUM | 2 | config.py |
| VAD Analyzer Cleanup | LOW | 1 | config.py |
| Redis Pool Staleness | LOW | 1 | redis_client.py |
| Database Client Lifecycle | MEDIUM | 2 | turso_client.py |
| Event Handler Deregistration | MEDIUM | 1 | main.py |
| Logger Sink Cleanup | LOW | 1 | logging_config.py |
| Long-Running Sync Tasks | MEDIUM | 1 | config_sync.py |
| **TOTAL** | | **34** | **14 files** |

---

### RECOMMENDATIONS

1. **Implement Cache Size Limits**: Add `maxsize` or LRU eviction to character prompt cache, explore cache, and all config caches.
2. **Explicit Cache Clearing**: Add cleanup methods to module-level globals that explicitly clear dictionaries and lists.
3. **Close Shared Clients**: Add explicit `close()` methods to shared aiohttp/httpx clients and call on shutdown.
4. **Track All Background Tasks**: Use a centralized task tracker for all async tasks with guaranteed cleanup.
5. **WebSocket Cleanup Guarantees**: Wrap WebSocket in context manager or ensure cleanup is called even on exception.
6. **Logger Handler Cleanup**: Store sink references and remove handlers on app shutdown.
7. **Use Context Managers**: Implement `__enter__/__exit__` for services that need cleanup.
8. **Event Handler Deregistration**: Add deregistration calls for all event handlers on session cleanup.
9. **VAD Model Cleanup**: Call explicit `cleanup()` method if it exists before setting to None.
10. **Redis Pool Refresh**: Allow pool to be recreated if it becomes stale.