Stage 4: honest health, explicit error policy, event-bus integrity
Engine mode is now explicit: USE_MOCK_SIP=true is the only way to get the mock engine; an unconfigured trunk fails startup with guidance instead of silently degrading. Root-caused why the engine always ran mock: nested pydantic-settings never read .env (no env_file on the sub-settings classes) — all 8 now declare it. /health stops lying: reports engine mode (sippy|mock), a live DB SELECT 1, trunk registration state with reason, and TTS/STT availability from their last real request; "healthy" now requires ready + db + sippy + registered trunk. Error policy: leaf services (tts/transcription/llm_client) raise and track availability; call-loop callers catch, publish EventType.ERROR naming the failed service, and apply an explicit fallback. Persistence writes get one bounded 3x exponential retry, then an ERROR log — no more silent data loss. Event bus: a full subscriber queue drops its oldest event (counted) instead of silently evicting the subscription; subscribe(replay_last=N) delivers the advertised history replay, used by /ws/events (25). Receptionist correctness: a matched TAKE_MESSAGE rule beats the LLM; voicemail polls for early hangup and stops/transcribes/hangs up in finally; RecordingSession finally keeps its leg_ids so taps detach. Dead code removed: models/contact.py + Contact table, dtmf_buffer, transcribe_stream stub, SMS stub in notification.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,7 @@ class RecordingService:
|
||||
filepath_agent=filepath_agent,
|
||||
started_at=datetime.now(),
|
||||
sample_rate=self._sample_rate,
|
||||
leg_ids=leg_ids,
|
||||
)
|
||||
|
||||
# Start PJSUA2 recording if media pipeline is available
|
||||
@@ -159,26 +160,38 @@ class RecordingService:
|
||||
|
||||
@staticmethod
|
||||
async def _persist_recording(session: "RecordingSession") -> None:
|
||||
"""Write a recordings row for this session. Failures are non-fatal."""
|
||||
try:
|
||||
import uuid as _uuid
|
||||
from db.database import RecordingRecord, get_session_factory
|
||||
"""Write a recordings row for this session, with bounded retry.
|
||||
|
||||
async with get_session_factory()() as db:
|
||||
db.add(RecordingRecord(
|
||||
id=f"rec_{_uuid.uuid4().hex[:10]}",
|
||||
call_id=session.call_id,
|
||||
path=session.filepath_mixed or "",
|
||||
format="wav",
|
||||
duration_s=float(session.duration_seconds or 0),
|
||||
size_bytes=int(session.file_size_bytes or 0),
|
||||
channels=1,
|
||||
started_at=session.started_at,
|
||||
ended_at=session.stopped_at,
|
||||
))
|
||||
await db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"Recording persistence failed: {e}")
|
||||
Non-fatal for the call, but a lost row means the dashboard can
|
||||
never find the WAV — so failures log at ERROR, not warning.
|
||||
"""
|
||||
import uuid as _uuid
|
||||
|
||||
from db.database import RecordingRecord, session_scope
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
async with session_scope() as db:
|
||||
db.add(RecordingRecord(
|
||||
id=f"rec_{_uuid.uuid4().hex[:10]}",
|
||||
call_id=session.call_id,
|
||||
path=session.filepath_mixed or "",
|
||||
format="wav",
|
||||
duration_s=float(session.duration_seconds or 0),
|
||||
size_bytes=int(session.file_size_bytes or 0),
|
||||
channels=1,
|
||||
started_at=session.started_at,
|
||||
ended_at=session.stopped_at,
|
||||
))
|
||||
return
|
||||
except Exception as e:
|
||||
if attempt == 2:
|
||||
logger.error(
|
||||
f"Recording row lost for {session.call_id} "
|
||||
f"(path={session.filepath_mixed}): {e}"
|
||||
)
|
||||
return
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
|
||||
async def _recording_timeout(self, call_id: str) -> None:
|
||||
"""Auto-stop recording after max duration."""
|
||||
@@ -239,6 +252,7 @@ class RecordingSession:
|
||||
filepath_agent: Optional[str] = None,
|
||||
started_at: Optional[datetime] = None,
|
||||
sample_rate: int = 16000,
|
||||
leg_ids: Optional[list[str]] = None,
|
||||
):
|
||||
self.call_id = call_id
|
||||
self.filepath_mixed = filepath_mixed
|
||||
@@ -249,7 +263,7 @@ class RecordingSession:
|
||||
self.duration_seconds: Optional[int] = None
|
||||
self.file_size_bytes: Optional[int] = None
|
||||
self.sample_rate = sample_rate
|
||||
self._leg_ids: list[str] = []
|
||||
self._leg_ids: list[str] = list(leg_ids or [])
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user