Stage 3: composition root, core↔services cycle break, shared REST+MCP data layer #3
Reference in New Issue
Block a user
Delete Branch "feature/stage3-structure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Third stage of the architecture-review roadmap. Stacked on #2 — merge #1 → #2 → this.
What changes
Composition root moves to
main.py's lifespan. The gateway was composing its own services (with function-localfrom services...imports working around a core↔services import cycle), and the lifespan pokedgateway._recording_servicefrom outside. Now every service is built and wired in one place;core/no longer importsservices/anywhere — the cycle is dead because main sits above both.AIPSTNGateway.from_config()is deleted; tests compose explicitly.Inbound-call policy moves to the receptionist. The full
_on_sip_incoming_callbody (routing evaluation, reject/answer, screening dispatch) is nowReceptionistService.on_inbound_call, wired as the engine'son_incoming_callcallback by the lifespan. Receptionist dependencies (tts / transcription / recording / routing) are constructor-injected — no moregateway._ttsreach-through, and the import of hold_slayer's private_get_llmis replaced by a sharedservices.llm_client.get_llm.The gateway shrinks to what it should be: live-call ops, device registry, tracked tasks. Hold-slayer launch goes through a mode-handler registry (
register_mode_handler(CallMode.HOLD_SLAYER, ...)) so the gateway no longer knows the service's type.CallManagertakeson_call_endedin its constructor.build_sip_engine()is a pure function taking explicit callbacks.api/routing.pygets the routing service fromapp.statevia a proper dependency instead ofgateway._routing.One data layer for both surfaces.
db.session_scope()is the single session convention (get_dbwraps it for REST; MCP/services use it directly — kills theget_session_factoryvsget_dbsplit).services/call_persistence.pynow holds the shared query/write functions plus the singleStoredCallFlow→CallFlowmapper;api/call_flows.py,api/call_history.py, and the six DB-touching MCP tools are thin wrappers over them, so REST and MCP can't drift. The three private_call_legsscans (gateway transfer/hangup, REST dtmf, MCP dtmf) now useCallManager.legs_for_call().Tests & verification
tests/test_structure.py: mode-handler launch, constructoron_call_endedhook, receptionist inbound answer/reject paths, and call-flow CRUD + history routes exercised over httpx-ASGI against a real SQLite database through the shared layer (aiosqliteadded to dev extras).main.Notes
services/call_analytics.pydeletion described in #1 actually lands in this commit — a stash round-trip during lint-baseline comparison had unstaged thegit rm, so it rode the working tree untilgit add -uhere. End state is as described in #1.🤖 Generated with Claude Code