the transcript is being saved via the <vscode-file...
# ask-ai
a
the transcript is being saved via the on_session_end callback which acts like a "webhook" but whats happening is sometimes it is failing since i am using callback at the end How It Works:
Copy code
python
<@U07SJDE11GF>.rtc_session(
    agent_name="emi-reminder-agent-test2",
    type=WorkerType.ROOM,
    on_session_end=on_session_end,  # ← This is the "webhook"
)
***When Session Ends →
on_session_end()
is called:*** 1. ***Extracts transcript*** from
ctx.make_session_report()
2. ***Formats transcript*** from LiveKit's session report format 3. ***Saves to database*** via
CallRepository.queue_call_record()
--- ## ⚠️ The Problem You Identified ### Issue #1:
on_session_end
Only Fires When Session Completes Successfully
## ⚠️ The Problem You Identified ### Issue #1:
on_session_end
Only Fires When Session Completes Successfully
***Evidence from logs:***
Copy code
:white_check_mark: SUCCESS: ":floppy_disk: [SessionReport] Transcript persisted for call 7c530579... (10 items)"
:white_check_mark: SUCCESS: ":floppy_disk: [SessionReport] Transcript persisted for call 53e71e23... (17 items)"

:x: FAILURE: ":x: Unable to build session report: Cannot prepare report, no AgentSession was found"
***Why it fails:*** - If agent crashes ***before***
session.start()
→ No AgentSession exists - If agent disconnects abruptly →
on_session_end
may not fire - If SIP call drops → Session might not end gracefully ### Issue #2: Small Transcript Counts Looking at successful saves:
Copy code
:white_check_mark: "Transcript persisted... (2 items)"   ← Very short
:white_check_mark: "Transcript persisted... (10 items)"  ← Reasonable
:white_check_mark: "Transcript persisted... (17 items)"  ← Good
***Many calls have only 2 items***, which suggests: - Calls are ending very early (probably just greeting) - Or the session is terminated before full conversation --- ## 🔍 Root Cause Analysis ### Why Transcripts Are Missing: | Scenario |
on_session_end
Called? | Transcript Saved? | |----------|-------------------------|-------------------| | ***Normal completion*** | ✅ Yes | ✅ Yes (2-17 items) | | ***Agent crash before session.start()*** | ❌ No | ❌ No - "no AgentSession found" | | ***Abrupt SIP disconnect*** | ❌ Maybe not | ❌ No | | ***Agent dispatch fails*** | ❌ No | ❌ No | | ***Network issue during call*** | ❌ Maybe not | ❌ No | ### The Core Problem:
t
Thanks for reaching out, Aryan. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a clear write-up of the problem. I’ll respond to the two issues you highlighted and give concrete, low-effort options you can try next. Acknowledging what you asked: you’re seeing missed or tiny transcripts because
on_session_end
only runs when the agent process survives to a graceful end, so crashes/abrupt disconnects or sessions that never start lead to missing reports. Recommended fixes (ordered from easiest/fastest to most robust): 1) Persist transcript chunks in real time (best immediate improvement) • Subscribe to the transcription/conversation events (e.g.,
user_input_transcribed
/
conversation_item_added
) and immediately append final chunks to your DB or a queue. • This preserves partial transcripts if the agent dies mid-call. 2) Add a shutdown callback to attempt final save • Use
ctx.add_shutdown_callback()
to run a last-attempt save on agent shutdown. • Note: shutdown callbacks have a limited timeout and won’t run if the process is killed instantly, but they catch many graceful/controlled shutdowns. 3) Start sessions with recording enabled • Call
session.start(..., record=True)
so LiveKit stores the session data independently of your agent. • You can retrieve session reports from LiveKit after the fact even if the agent crashed. 4) Combine with server-side webhooks for room lifecycle • Configure LiveKit webhooks (e.g.,
room_finished
) to notify your backend. From that webhook you can fetch recordings or trigger a retry of transcript collection independent of the agent process. 5) Add diagnostics to distinguish short transcripts vs early terminations • Log participant events and
agent_state_changed
and save an exit reason (crash, SIP drop, dispatched-failed) with each call record to decide whether a short transcript is legitimate or due to an error. Immediate next step I can help with • Which of the above do you want to implement first? If you pick one, tell me which language/stack your agent uses (Python or Node) and I’ll give a minimal code snippet and exact event names to wire up. Sources: Agent session | LiveKit docs | Job lifecycle | LiveKit docs | Transcriber | LiveKit docs | Agents (Node.js) | livekit/livekit_composite | DeepWiki | https://blog.livekit.io/streamline-troubleshooting-with-agent-observability/#/portal|
a
which is the esiest and sure way of getting all the transcripts properly
t
Short answer: enable session recording (
record=True
) when you start the session, and stream transcript chunks as they arrive as a backup. That combination is the simplest and most reliable. Why this is best: •
record=True
ensures LiveKit stores the full session independently of your agent process, so you can always retrieve the session report after crashes or abrupt disconnects. • Real-time chunk persistence (subscribe to transcription / conversation events and save final chunks immediately) guarantees you keep partial transcripts if recording/report retrieval is delayed or the recording missed anything. Recommended minimal setup 1. Start session with recording:
Copy code
await session.start(agent=agent, room=ctx.room, record=True)
1. Persist transcript chunks as they arrive: • Subscribe to
user_input_transcribed
or
conversation_item_added
. • On final segments (
is_final
), append to DB or enqueue for processing. Optional reliability additions • Add
ctx.add_shutdown_callback()
to attempt a last save on graceful shutdown. • Configure a LiveKit webhook (e.g.,
room_finished
) to trigger post-call retrieval of the session report if your agent is unavailable. Which do you want first — a short code snippet for streaming transcripts into your DB, or instructions for fetching the recorded session report after a crash?
a
@session.on("user_input_transcribed") async def on_user_transcribed(event): """Save user transcript chunks in real-time""" try: if hasattr(event, 'is_final') and event.is_final: transcript_entry = { "speaker": "user", "text": event.text if hasattr(event, 'text') else str(event), "timestamp": datetime.utcnow().isoformat(), "relative_time": (datetime.utcnow() - agent.call_start_time).total_seconds() } transcript_buffer.append(transcript_entry) emi_data.full_transcript.append(transcript_entry) # Periodic save every 5 entries if len(transcript_buffer) >= 5: record = CallRecordModel( callId=call_id, fullTranscript=transcript_buffer.copy(), updatedAt=datetime.utcnow() ) await CallRepository.queue_call_record(record) logger.info(f"💾 [REAL-TIME] Saved transcript chunk ({len(transcript_buffer)} entries)") transcript_buffer.clear() except Exception as e: logger.error(f"❌ Error saving user transcript: {e}") @session.on("agent_speech_committed") async def on_agent_speech(event): """Save agent speech chunks in real-time""" try: if hasattr(event, 'text') and event.text: transcript_entry = { "speaker": "assistant", "text": event.text, "timestamp": datetime.utcnow().isoformat(), "relative_time": (datetime.utcnow() - agent.call_start_time).total_seconds() } transcript_buffer.append(transcript_entry) emi_data.full_transcript.append(transcript_entry) # Periodic save every 5 entries if len(transcript_buffer) >= 5: record = CallRecordModel( callId=call_id, fullTranscript=transcript_buffer.copy(), updatedAt=datetime.utcnow() ) await CallRepository.queue_call_record(record) logger.info(f"💾 [REAL-TIME] Saved transcript chunk ({len(transcript_buffer)} entries)") transcript_buffer.clear() except Exception as e: logger.error(f"❌ Error saving agent speech: {e}") # ============================================================================ # STEP: ADD SHUTDOWN CALLBACK FOR LAST TRANSCRIPT SAVE # ============================================================================ async def save_final_transcript(): """Save final transcript on shutdown""" try: # Save any remaining buffered transcript if transcript_buffer or emi_data.full_transcript: final_transcript = emi_data.full_transcript if final_transcript: logger.info(f"💾 [SHUTDOWN] Saving final transcript: {len(final_transcript)} entries") record = CallRecordModel( callId=call_id, fullTranscript=final_transcript, updatedAt=datetime.utcnow(), callEndTime=datetime.utcnow() ) await CallRepository.queue_call_record(record) logger.info(f"✅ [SHUTDOWN] Final transcript saved successfully") except Exception as e: logger.error(f"❌ [SHUTDOWN] Error saving final transcript: {e}") ctx.add_shutdown_callback(save_final_transcript) logger.info("✅ [AGENT] Shutdown callback registered for transcript backup") logger.info(f"🎙️ [AGENT] Session recording ENABLED for transcript backup") await session.start( agent=agent, room=ctx.room, room_input_options=RoomInputOptions( audio_enabled=True, text_enabled=False, ), room_output_options=RoomOutputOptions( audio_enabled=True, ), record=True, # ✅ ENABLE SESSION RECORDING for transcript backup ) logger.info(f"✅ [AGENT] EMI session completed successfully")this is ewhat i wrote rker:✅ Silero VAD configured: 450ms silence threshold, 0.4 activation (confirmation-optimized) INFOemi agent worker✅ Silero VAD configured: 450ms silence threshold, 0.4 activation (confirmation-optimized) {"level": "INFO", "name": "emi_agent_worker", "message": "✅ Silero VAD configured: 450ms silence threshold, 0.4 activation (confirmation-optimized)", "pid": 31204, "job_id": "AJ_ttDoUpbK8HX8", "room_id": "RM_e8zk2pouyW38", "timestamp": "2025-12-23T081418.578621+00:00"} ERRORemi agent worker❌ EMI agent error: Cannot register an async callback with
.on()
. Use
asyncio.create_task
within your synchronous callback instead. ERRORemi agent worker❌ EMI agent error: Cannot register an async callback with
.on()
. Use
asyncio.create_task
within your synchronous callback instead. {"level": "ERROR", "name": "emi_agent_worker", "message": "❌ EMI agent error: Cannot register an async callback with
.on()
. Use
asyncio.create_task
within your synchronous callback instead.", "pid": 31204, "job_id": "AJ_ttDoUpbK8HX8", "room_id": "RM_e8zk2pouyW38", "timestamp": "2025-12-23T081418.621860+00:00"} Traceback (most recent call last): File "/Users/aryansharma/Desktop/Voice Ai Agent Indiap2p/livekit-voice-agent/Indiap2pVoiceAiAgents/src/agents/emi_agent_worker.py", line 966, in entrypoint @session.on("user_input_transcribed") ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/homebrew/Caskroom/miniconda/base/lib/python3.13/site-packages/livekit/rtc/event_emitter.py", line 172, in decorator self.on(event, callback) ~~~~~~~^^^^^^^^^^^^^^^^^ File "/opt/homebrew/Caskroom/miniconda/base/lib/python3.13/site-packages/livekit/rtc/event_emitter.py", line 161, in on raise ValueError( "Cannot register an async callback with
.on()
. Use
asyncio.create_task
within your synchronous callback instead." ) ValueError: Cannot register an async callback with
.on()
. Use
asyncio.create_task
within your synchronous callback instead. WARNINGemi agent worker⚠️ [FINALLY] No transcript to save (empty) INFOemi agent worker✅ [CLEANUP] No pending writes WARNINGemi agent worker⚠️ [FINALLY] No transcript to save (empty) {"level": "WARNING", "name": "emi_agent_worker", "message": "⚠️ [FINALLY] No transcript to save (empty)", "pid": 31204, "job_id": "AJ_ttDoUpbK8HX8", "room_id": "RM_e8zk2pouyW38", "timestamp": "2025-12-23T081418.622920+00:00"} INFOemi agent worker✅ [CLEANUP] No pending writes {"level": "INFO", "name": "emi_agent_worker", "message": "✅ [CLEANUP] No pending writes", "pid": 31204, "job_id": "AJ_ttDoUpbK8HX8", "room_id": "RM_e8zk2pouyW38", "timestamp": "2025-12-23T081418.622959+00:00"} DEBUGpymongo.topology{"message": "Server heartbeat succeeded", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true, "durationMS": 10074.618582963012, "reply": "{\"isWritablePrimary\": true, \"topologyVersion\": {\"processId\": {\"$oid\": \"69412ba80daa675be1de9642\"}}, \"maxBsonObjectSize\": 16777216, \"maxMessageSizeBytes\": 48000000, \"maxWriteBatchSize\": 100000, \"localTime\": {\"$date\": \"2025-12-23T081425.940Z\"}, \"logicalSessionTimeoutMinutes\": 30, \"connectionId\": 1499, \"maxWireVersion\": 27, \"ok\": 1.0}"} DEBUGpymongo.topology{"message": "Server heartbeat started", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true} DEBUGpymongo.topology{"message": "Server heartbeat succeeded", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true, "durationMS": 10069.249459018465, "reply": "{\"isWritablePrimary\": true, \"topologyVersion\": {\"processId\": {\"$oid\": \"69412ba80daa675be1de9642\"}}, \"maxBsonObjectSize\": 16777216, \"maxMessageSizeBytes\": 48000000, \"maxWriteBatchSize\": 100000, \"localTime\": {\"$date\": \"2025-12-23T081436.014Z\"}, \"logicalSessionTimeoutMinutes\": 30, \"connectionId\": 1499, \"maxWireVersion\": 27, \"ok\": 1.0}"} DEBUGpymongo.topology{"message": "Server heartbeat started", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true} DEBUGpymongo.topology{"message": "Server heartbeat succeeded", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true, "durationMS": 10071.388457959983, "reply": "{\"isWritablePrimary\": true, \"topologyVersion\": {\"processId\": {\"$oid\": \"69412ba80daa675be1de9642\"}}, \"maxBsonObjectSize\": 16777216, \"maxMessageSizeBytes\": 48000000, \"maxWriteBatchSize\": 100000, \"localTime\": {\"$date\": \"2025-12-23T081446.089Z\"}, \"logicalSessionTimeoutMinutes\": 30, \"connectionId\": 1499, \"maxWireVersion\": 27, \"ok\": 1.0}"} DEBUGpymongo.topology{"message": "Server heartbeat started", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true} DEBUGpymongo.topology{"message": "Server heartbeat succeeded", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true, "durationMS": 10042.52662503859, "reply": "{\"isWritablePrimary\": true, \"topologyVersion\": {\"processId\": {\"$oid\": \"69412ba80daa675be1de9642\"}}, \"maxBsonObjectSize\": 16777216, \"maxMessageSizeBytes\": 48000000, \"maxWriteBatchSize\": 100000, \"localTime\": {\"$date\": \"2025-12-23T081456.148Z\"}, \"logicalSessionTimeoutMinutes\": 30, \"connectionId\": 1499, \"maxWireVersion\": 27, \"ok\": 1.0}"} DEBUGpymongo.topology{"message": "Server heartbeat started", "topologyId": {"$oid": "694a4f57df074610f8e96577"}, "driverConnectionId": 1, "serverConnectionId": 1499, "serverHost": "localhost", "serverPort": 27017, "awaited": true} DEBUGlivekittungstenite:protocol666tungstenite:protocol - Received close frame: Some(CloseFrame { code: Normal, reason: "" }) DEBUGlivekittungstenite:protocol683tungstenite:protocol - Replying to close with Frame { header: FrameHeader { is_final: true, rsv1: false, rsv2: false, rsv3: fa INFOlivekitlivekit:room1400livekit:room - disconnected from room with reason: RoomClosed ERRORemi agent worker❌ Unable to build session report: Cannot prepare report, no AgentSession was found logs
t
Thanks — this pinpoints the immediate error: the Python SDK’s
.on()
requires a synchronous callback. Your async handlers raised
ValueError: Cannot register an async callback with .on()
. What to do next (concise, actionable): 1) Wrap async work inside a synchronous handler and spawn a task:
Copy code
def _sync_user_transcribed(event):
    asyncio.create_task(_handle_user_transcribed(event))

async def _handle_user_transcribed(event):
    try:
        if getattr(event, "is_final", False):
            entry = {
                "speaker": "user",
                "text": getattr(event, "text", str(event)),
                "timestamp": datetime.utcnow().isoformat(),
                "relative_time": (datetime.utcnow() - agent.call_start_time).total_seconds(),
            }
            transcript_buffer.append(entry)
            emi_data.full_transcript.append(entry)

            if len(transcript_buffer) >= 5:
                record = CallRecordModel(callId=call_id, fullTranscript=transcript_buffer.copy(), updatedAt=datetime.utcnow())
                await CallRepository.queue_call_record(record)
                <http://logger.info|logger.info>(f"💾 [REAL-TIME] Saved transcript chunk ({len(transcript_buffer)} entries)")
                transcript_buffer.clear()
    except Exception as e:
        logger.error(f"❌ Error saving user transcript: {e}")

session.on("user_input_transcribed", _sync_user_transcribed)
2) Do the same for
agent_speech_committed
(sync wrapper ->
asyncio.create_task
-> async handler). 3) Keep your shutdown callback as-is but ensure it’s async and added via
ctx.add_shutdown_callback(save_final_transcript)
. That will run on graceful shutdown; it’s a good last-attempt saver. 4) Continue using
record=True
on
session.start(...)
— this gives you the most reliable fallback (LiveKit-stored session report retrievable after crashes). Quick checks • Ensure
agent.call_start_time
exists before first event. • Protect shared lists (
transcript_buffer
,
emi_data.full_transcript
) if accessed from multiple tasks (use an
asyncio.Lock()
). • Log exceptions inside the task so failures don’t silently vanish. If you want, I can: • Provide the wrapped handler for
agent_speech_committed
and a sample
save_final_transcript
that merges buffered and persisted chunks safely. • Show how to fetch the recorded session report after a crash. Which would you like next?
a
transcript_buffer = [] # Buffer for real-time transcript chunks transcript_lock = asyncio.Lock() # Protect shared lists from race conditions # Async handler for user transcript async def _handle_user_transcribed(event): """Async handler to save user transcript chunks""" try: if getattr(event, "is_final", False): # Ensure call_start_time exists if not hasattr(agent, 'call_start_time') or agent.call_start_time is None: logger.warning("⚠️ agent.call_start_time not initialized, using current time") agent.call_start_time = datetime.utcnow() # ✅ CRITICAL FIX: Extract text correctly from event # Try transcript first (STT events), then text (agent events), then fallback text_content = getattr(event, "transcript", None) or getattr(event, "text", None) if not text_content: logger.warning(f"⚠️ No transcript/text found in event: {type(event)}") return entry = { "speaker": "user", "text": text_content, "timestamp": datetime.utcnow().isoformat(), "relative_time": (datetime.utcnow() - agent.call_start_time).total_seconds(), } async with transcript_lock: transcript_buffer.append(entry) emi_data.full_transcript.append(entry) # Periodic save every 5 entries if len(transcript_buffer) >= 5: record = CallRecordModel( callId=call_id, fullTranscript=transcript_buffer.copy(), updatedAt=datetime.utcnow(), # ✅ Don't include payloadData - already set by API server ) await CallRepository.queue_call_record(record) logger.info(f"💾 [REAL-TIME] Saved user transcript chunk ({len(transcript_buffer)} entries)") transcript_buffer.clear() except Exception as e: logger.error(f"❌ Error saving user transcript: {e}") import traceback traceback.print_exc() # Synchronous wrapper for user transcript def _sync_user_transcribed(event): """Synchronous wrapper that spawns async task""" asyncio.create_task(_handle_user_transcribed(event)) # Async handler for agent speech async def _handle_agent_speech(event): """Async handler to save agent speech chunks""" try: # ✅ Extract text from event (agent speech events use 'text' attribute) text_content = getattr(event, "text", None) if text_content: # Ensure call_start_time exists if not hasattr(agent, 'call_start_time') or agent.call_start_time is None: logger.warning("⚠️ agent.call_start_time not initialized, using current time") agent.call_start_time = datetime.utcnow() entry = { "speaker": "assistant", "text": text_content, "timestamp": datetime.utcnow().isoformat(), "relative_time": (datetime.utcnow() - agent.call_start_time).total_seconds(), } async with transcript_lock: transcript_buffer.append(entry) emi_data.full_transcript.append(entry) # Periodic save every 5 entries if len(transcript_buffer) >= 5: record = CallRecordModel( callId=call_id, fullTranscript=transcript_buffer.copy(), updatedAt=datetime.utcnow(), # ✅ Don't include payloadData - already set by API server ) await CallRepository.queue_call_record(record) logger.info(f"💾 [REAL-TIME] Saved agent speech chunk ({len(transcript_buffer)} entries)") transcript_buffer.clear() except Exception as e: logger.error(f"❌ Error saving agent speech: {e}") import traceback traceback.print_exc() # Synchronous wrapper for agent speech def _sync_agent_speech(event): """Synchronous wrapper that spawns async task""" asyncio.create_task(_handle_agent_speech(event)) # Register synchronous callbacks session.on("user_input_transcribed", _sync_user_transcribed) session.on("agent_speech_committed", _sync_agent_speech) logger.info("✅ [AGENT] Real-time transcript event handlers registered") # ============================================================================ # STEP: ADD SHUTDOWN CALLBACK FOR LAST TRANSCRIPT SAVE # ============================================================================ async def save_final_transcript(): """Save final transcript on shutdown""" try: async with transcript_lock: # Save any remaining buffered transcript if transcript_buffer or emi_data.full_transcript: final_transcript = emi_data.full_transcript if final_transcript: logger.info(f"💾 [SHUTDOWN] Saving final transcript: {len(final_transcript)} entries") record = CallRecordModel( callId=call_id, fullTranscript=final_transcript, updatedAt=datetime.utcnow(), callEndTime=datetime.utcnow(), # ✅ Include payloadData as backup in case API server data was lost payloadData={ "language": emi_data.language, "loanId": emi_data.loan_id, "phoneNumber": emi_data.phone_number, "borrowerName": emi_data.borrower_name, "emiAmount": emi_data.emi_amount, "dueDate": emi_data.due_date, }, ) await CallRepository.queue_call_record(record) logger.info(f"✅ [SHUTDOWN] Final transcript saved successfully") except Exception as e: logger.error(f"❌ [SHUTDOWN] Error saving final transcript: {e}") import traceback traceback.print_exc() ctx.add_shutdown_callback(save_final_transcript) logger.info("✅ [AGENT] Shutdown callback registered for transcript backup") logger.info(f"🎙️ [AGENT] Session recording ENABLED for transcript backup") await session.start( agent=agent, room=ctx.room, room_input_options=RoomInputOptions( audio_enabled=True, text_enabled=False, ), room_output_options=RoomOutputOptions( audio_enabled=True, ), record=True, # ✅ ENABLE SESSION RECORDING for transcript backup ) logger.info(f"✅ [AGENT] EMI session completed successfully") except Exception as e: logger.error(f"❌ EMI agent error: {e}") import traceback traceback.print_exc() finally: try: if 'emi_data' in locals() and hasattr(emi_data, 'full_transcript'): # Use lock if it exists if 'transcript_lock' in locals(): async with transcript_lock: if len(emi_data.full_transcript) > 0: logger.info(f"💾 [FINALLY] Saving final transcript: {len(emi_data.full_transcript)} entries") record = CallRecordModel( callId=call_id, fullTranscript=emi_data.full_transcript, updatedAt=datetime.utcnow(), callEndTime=datetime.utcnow(), ) await CallRepository.queue_call_record(record) logger.info(f"✅ [FINALLY] Final transcript saved successfully") else: logger.warning(f"⚠️ [FINALLY] No transcript to save (empty)") else: # Fallback if lock doesn't exist if len(emi_data.full_transcript) > 0: logger.info(f"💾 [FINALLY] Saving final transcript: {len(emi_data.full_transcript)} entries") record = CallRecordModel( callId=call_id, fullTranscript=emi_data.full_transcript, updatedAt=datetime.utcnow(), callEndTime=datetime.utcnow(), ) await CallRepository.queue_call_record(record) logger.info(f"✅ [FINALLY] Final transcript saved successfully") else: logger.warning(f"⚠️ [FINALLY] No transcript to save (empty)") except Exception as transcript_error: logger.error(f"❌ [FINALLY] Error saving final transcript: {transcript_error}") # ✅ CRITICAL FIX: Wait for ALL writes before closing try: if db_manager.connected: queue_size = db_manager.write_queue.qsize() if queue_size > 0: logger.info(f"⏳ [CLEANUP] Waiting for {queue_size} pending writes...") await db_manager.wait_for_writes(timeout=15.0) logger.info("✅ [CLEANUP] All writes completed") else: logger.info("✅ [CLEANUP] No pending writes") # Small delay to ensure everything is flushed await asyncio.sleep(0.5) except Exception as e: logger.error(f"❌ [CLEANUP] Cleanup error: {e}") import traceback traceback.print_exc() os this is how my transcription hook is working but if call is discpconnectingh in the middle only user transcription is comin
t
Thanks — you’ve built a solid pipeline. You’re seeing only user transcripts when the call disconnects mid-call because agent speech events sometimes aren’t emitted/committed before the disconnect or your agent’s finalization logic doesn't get time to persist the last agent entries. Below are focused fixes and checks to ensure you capture agent side text as reliably as the user side. What to check first (quick diagnostics) • Confirm agent speech events are being emitted before disconnect: enable debug logging for
agent_speech_committed
and check timestamps vs disconnect. • Verify
event.text
exists on the agent events you expect. Log the raw event when you see only user transcripts. • Check whether the disconnect is abrupt (process killed / network) — shutdown callbacks won’t run if the process is killed immediately. Concrete fixes (apply in this order) 1) Flush agent speech immediately (don’t wait for buffer size) • Agent speech may be committed only at the end; when it appears, persist it immediately instead of waiting for 5 items. Example change inside `_handle_agent_speech`:
Copy code
# after appending entry
await CallRepository.queue_call_record(CallRecordModel(callId=call_id, fullTranscript=[entry], updatedAt=datetime.utcnow()))
This ensures the agent chunk is persisted even if the process dies right after. 2) Force agent finalization on disconnect events • Register listeners for participant/room lifecycle events (e.g., participant left, connection state changed, room closed) and trigger an immediate save task. • Use synchronous wrapper +
asyncio.create_task
same as you did:
Copy code
def _sync_participant_left(event):
    asyncio.create_task(_handle_disconnect_event(event))
session.on("participant_disconnected", _sync_participant_left)
In
_handle_disconnect_event
call your final save logic (with lock) to persist any buffered data. 3) Ensure text output for agent speech (so
event.text
exists) • If your agent synthesizes speech without generating text, enable the engine/path that produces transcription text or ensure the agent commit step populates
text
. If agent-side TTS bypasses text, produce the text before committing. 4) Shorten shutdown dependency and wait for pending writes • You already wait for pending DB writes; make sure
db_manager.wait_for_writes(timeout=...)
is sufficient and avoid very long blocking during shutdown. • Add a small synchronous flush on critical events (disconnect/room closed) to reduce reliance on shutdown callbacks. 5) Keep
record=True
as a final fallback • Session recording stores the full session independent of your process and is the most reliable recovery if your agent crashes before persisting final agent chunks. • After a crash, fetch the session report to recover missing agent speech. Thread-safety and robustness tips • You have
transcript_lock
— good. Also protect any in-memory flags like
agent.call_start_time
. • When creating tasks, wrap with try/except and log exceptions to avoid silent failures. • Consider batching writes by time (e.g., every 5–10s) in addition to item count, so low-traffic calls still flush regularly. If you want, I can: • Provide the wrapped disconnect handler snippet that triggers an immediate flush (with lock + safe DB write). • Provide a minimal example to persist every agent chunk immediately while still keeping your 5-item buffer for user chunks. Which would you prefer?