From c5e7dc09aed302619979d6a1bd66f9b046ee2ce9 Mon Sep 17 00:00:00 2001 From: kimi Date: Thu, 19 Mar 2026 19:30:19 -0400 Subject: [PATCH] refactor: break up lifespan() into focused helpers Extract _init_services(), _auto_prune(), _register_error_recorder(), and _shutdown() from the 142-line lifespan() context manager, reducing it to ~25 lines that read as a clear startup/shutdown sequence. Fixes #514 Co-Authored-By: Claude Opus 4.6 --- src/dashboard/app.py | 98 ++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/src/dashboard/app.py b/src/dashboard/app.py index db7e115..c7ef177 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -329,33 +329,21 @@ async def _discord_token_watcher() -> None: logger.warning("Discord auto-start failed: %s", exc) -@asynccontextmanager -async def lifespan(app: FastAPI): - """Application lifespan manager with non-blocking startup.""" - - # Validate security config (no-op in test mode) +def _init_services() -> None: + """Validate config, enable event persistence, and init Spark engine.""" from config import validate_startup + from infrastructure.events.bus import init_event_bus_persistence + from spark.engine import get_spark_engine validate_startup() - - # Enable event persistence (unified EventBus + swarm event_log) - from infrastructure.events.bus import init_event_bus_persistence - init_event_bus_persistence() - # Create all background tasks without waiting for them - briefing_task = asyncio.create_task(_briefing_scheduler()) - thinking_task = asyncio.create_task(_thinking_scheduler()) - loop_qa_task = asyncio.create_task(_loop_qa_scheduler()) - presence_task = asyncio.create_task(_presence_watcher()) - - # Initialize Spark Intelligence engine - from spark.engine import get_spark_engine - if get_spark_engine().enabled: logger.info("Spark Intelligence active — event capture enabled") - # Auto-prune old vector store memories on startup + +def _auto_prune() -> None: + """Run startup housekeeping: prune memories, thoughts, events, and check vault size.""" if settings.memory_prune_days > 0: try: from timmy.memory_system import prune_memories @@ -373,7 +361,6 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.debug("Memory auto-prune skipped: %s", exc) - # Auto-prune old thoughts on startup if settings.thoughts_prune_days > 0: try: from timmy.thinking import thinking_engine @@ -391,7 +378,6 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.debug("Thought auto-prune skipped: %s", exc) - # Auto-prune old system events on startup if settings.events_prune_days > 0: try: from swarm.event_log import prune_old_events @@ -409,7 +395,6 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.debug("Event auto-prune skipped: %s", exc) - # Warn if memory vault exceeds size limit if settings.memory_vault_max_mb > 0: try: vault_path = Path(settings.repo_root) / "memory" / "notes" @@ -425,17 +410,9 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.debug("Vault size check skipped: %s", exc) - # Start Workshop presence heartbeat with WS relay - from dashboard.routes.world import broadcast_world_state - from timmy.workshop_state import WorkshopHeartbeat - workshop_heartbeat = WorkshopHeartbeat(on_change=broadcast_world_state) - await workshop_heartbeat.start() - - # Start chat integrations in background - chat_task = asyncio.create_task(_start_chat_integrations_background()) - - # Register session logger with error capture (breaks infrastructure → timmy circular dep) +def _register_error_recorder() -> None: + """Wire the session logger into the error-capture system.""" try: from infrastructure.error_capture import register_error_recorder from timmy.session_logger import get_session_logger @@ -444,18 +421,18 @@ async def lifespan(app: FastAPI): except Exception: logger.debug("Failed to register error recorder") - logger.info("✓ Dashboard ready for requests") - yield - - # Cleanup on shutdown +async def _shutdown( + tasks: list[asyncio.Task], + workshop_heartbeat: object, +) -> None: + """Stop integrations, close sessions, and cancel background tasks.""" from integrations.chat_bridge.vendors.discord import discord_bot from integrations.telegram_bot.bot import telegram_bot await discord_bot.stop() await telegram_bot.stop() - # Close MCP tool server sessions try: from timmy.mcp_tools import close_mcp_sessions @@ -463,15 +440,46 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.debug("MCP shutdown: %s", exc) - await workshop_heartbeat.stop() + await workshop_heartbeat.stop() # type: ignore[union-attr] - for task in [briefing_task, thinking_task, chat_task, loop_qa_task, presence_task]: - if task: - task.cancel() - try: - await task - except asyncio.CancelledError: - pass + for task in tasks: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager with non-blocking startup.""" + _init_services() + _auto_prune() + + # Create all background tasks without waiting for them + bg_tasks = [ + asyncio.create_task(_briefing_scheduler()), + asyncio.create_task(_thinking_scheduler()), + asyncio.create_task(_loop_qa_scheduler()), + asyncio.create_task(_presence_watcher()), + ] + + # Start Workshop presence heartbeat with WS relay + from dashboard.routes.world import broadcast_world_state + from timmy.workshop_state import WorkshopHeartbeat + + workshop_heartbeat = WorkshopHeartbeat(on_change=broadcast_world_state) + await workshop_heartbeat.start() + + # Start chat integrations in background + bg_tasks.append(asyncio.create_task(_start_chat_integrations_background())) + + _register_error_recorder() + logger.info("✓ Dashboard ready for requests") + + yield + + await _shutdown(bg_tasks, workshop_heartbeat) app = FastAPI(