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

57 lines
2.9 KiB
Markdown

---
description: composition-root lifespan, nested MCP http_app lifespan, app.state wiring, route/mount ordering, honest /health
paths:
- "main.py"
- "api/deps.py"
- "core/gateway.py"
---
# Lifespan, composition root & route ordering
[main.py](../../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](../../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.