Files
hold-slayer/.claude/rules/lifespan-wiring.md
Robert Helewka 4a3c14d4af
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s
docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering
authentication surfaces, outbound-call safety invariants, and other project
conventions to guide Claude's understanding of critical system behaviors.
2026-07-28 19:01:38 -04:00

2.9 KiB

description, paths
description paths
composition-root lifespan, nested MCP http_app lifespan, app.state wiring, route/mount ordering, honest /health
main.py
api/deps.py
core/gateway.py

Lifespan, composition root & route ordering

main.py's lifespan is the composition root: it builds the gateway and every service, wires them by constructor/registration, and hangs the long-lived ones on app.state. This is the one place dependencies are assembled.

  • Build services here, inject them — nothing self-constructs its deps. The gateway, classifier, transcription, TTS, routing, recording, receptionist, and notification services are all constructed in the lifespan and wired together (e.g. the receptionist receives tts/transcription/recording/routing; launch_hold_slayer is registered as the HOLD_SLAYER mode handler). A new service is built here and passed in, not instantiated deep in a call path.

  • The MCP sub-app's lifespan MUST be nested. The lifespan opens async with mcp_http_app.lifespan(app): around all startup. FastMCP's streamable-HTTP session manager is initialised inside its lifespan; mount the app without entering that context and every /mcp request 500s ("session manager not initialised" / "Task group is not initialized"). Keep the nesting. This is the same landmine across the estate's mounted-MCP services.

  • app.state is the handoff to request handlers. The lifespan sets app.state.gateway, .routing_service, .transcription_service, .notification_service, .recording_service. Dependencies in api/deps.py read these and raise 503 if not yet set. MCP tools reach the gateway via the lazy _get_gateway_instance resolver. Don't reach for module-level globals; go through app.state.

  • Route/mount registration order is load-bearing:

    1. call_history router registers before calls — both live under /api/v1/calls, and calls' GET /{call_id} would otherwise swallow the literal path history. Keep history first.
    2. The "/mcp" mount and all API/WS/health routes register before the "/" static dashboard mount — a root mount matches every path, so anything after it is unreachable. The dashboard mount stays last, and only when dashboard/build/ exists.
  • /health is honest by construction. healthy = real (non-MockSIPEngine) engine and registered trunk and reachable DB; it also reports STT/TTS last-known reachability via _availability. Don't relax any of these to make a probe pass — a degraded gateway must read as degraded, with the reason visible.

  • Shutdown reverses startup. Stop notifications, stop the gateway (which cancels tracked tasks, ends active calls, stops SIP then media), close the DB. New long-lived resources get a matching teardown here — don't leak a task or a client across restarts.