bulky-byte-27418
04/22/2026, 8:02 AMasync def save_transcript_and_notify():
try:
raw_conversation = {}
try:
if "session" in dir() or "session" in vars():
raw_conversation = session.history.to_dict() if hasattr(session, "history") else {}
except NameError:
raw_conversation = {}
processed_transcript = process_transcript(raw_conversation, call_id, phone_number, ctx.room.name, request_id)
if isinstance(processed_transcript.get("conversation"), list):
clean_transcript = processed_transcript
else:
clean_transcript = create_clean_transcript(processed_transcript)
if isinstance(processed_transcript.get("conversation"), list):
clean_transcript = processed_transcript
else:
clean_transcript = create_clean_transcript(processed_transcript)
# Disabled to save on egress costs
recording_file_path = ""
transcript_data = {
"request_id": request_id,
"call_id": call_id,
"phone_number": phone_number,
"company_ctx": company_ctx,
"num_questions":num_questions,
"room_name": ctx.room.name,
"start_time": start_time,
"end_time": datetime.datetime.now(),
"raw_conversation": raw_conversation,
"processed_transcript": clean_transcript,
"recording_url": recording_file_path,
"metadata": {
"prompt": prompt,
"first_msg": first_msg,
"last_msg": last_msg,
"prompt_type": prompt_type,
"supplementary_questions": supplementary_questions,
"trunk_id": trunk_id
},
"processed_at": datetime.datetime.now()
}
await db.save_call_transcript(transcript_data)
<http://logger.info|logger.info>(f"Transcript saved to MongoDB for call {call_id}")
await db.update_call_status(call_id, "completed", {
"completed_at": datetime.datetime.now(),
"transcript_saved": True
})
if callback_url:
await send_callback_notification(call_id, callback_url, transcript_data)
except Exception as e:
logger.error(f"Error saving transcript: {e}")
above is my function for transcription processing.. the audio is there in s3 but its has not sended callbacktall-airline-16756
04/22/2026, 8:02 AMtall-airline-16756
04/22/2026, 8:02 AMctx.add_shutdown_callback() or equivalent)? If not, add a shutdown hook so the framework runs your cleanup during session shutdown.
1. Ensure all async calls are awaited
◦ In your snippet, verify send_callback_notification(...) is awaited and doesn’t spawn an unawaited background task.
1. Add timeouts and retries to external calls
◦ Ensure send_callback_notification uses an HTTP client with a reasonable timeout and retries so it doesn't hang during shutdown.
1. Increase shutdown timeout
◦ The agent waits (default ~10s) for shutdown hooks. If your processing + callback can take longer, increase shutdown_process_timeout so the process has time to finish.
1. Make the final work idempotent and resilient
◦ Persist transcript locally (or mark state in DB) before sending callback so a retry on next run can safely re-send if delivery failed.
◦ Log the callback request/response (status code, body, exception) so you can see if the HTTP request was attempted and what response returned.
1. Inspect logs for callback errors
◦ Share any logs that show exceptions from send_callback_notification or network errors. If there’s no log entry for the callback attempt, it likely never ran or was killed mid-call.
If you want, share:
• The code for send_callback_notification
• How/where you register shutdown hooks (or confirm you don’t)
• The configured shutdown timeout
• Any logs showing exceptions from that function
I’ll review and suggest the minimal code changes to ensure the callback completes reliably.
Sources: Why is my Agent insights data missing? | LiveKit | Why is my Agent insights data missing? | LiveKit | Job lifecycle | LiveKit Documentation | Agent session | LiveKit Documentation, with interactive elementsbulky-byte-27418
04/22/2026, 8:14 AMtall-airline-16756
04/22/2026, 8:15 AM# agent_server.py
from livekit.agents import AgentServer
import asyncio
async def finalize_transcription_and_send_callback(call_id, callback_url, transcript_data):
# use your real logic here (see HTTP example below)
await send_callback_notification(call_id, callback_url, transcript_data)
async def entrypoint(ctx):
async def shutdown_hook():
# ensure the finalization runs and is awaited
await finalize_transcription_and_send_callback(call_id, callback_url, transcript_data)
ctx.add_shutdown_callback(shutdown_hook)
# normal job logic here...
# Configure server: longer drain timeout and a little more time for shutdown hooks
server = AgentServer(
drain_timeout=2400, # wait up to 40 minutes for jobs to finish
shutdown_process_timeout=30, # allow shutdown hooks up to 30s
)
server.run(entrypoint)
2) Ensure your process receives signals (startup script)
#!/usr/bin/env bash
set -euo pipefail
# start the agent process with exec so signals are propagated
exec python agent_server.py
3) Graceful session shutdown usage (inside job when ending)
# when you decide to shutdown a session
await session.shutdown(drain=True) # request draining before exit
4) Safe async HTTP callback with timeout + retries (Python, aiohttp)
import aiohttp
import asyncio
async def send_callback_notification(call_id, callback_url, payload,
timeout_sec=5, max_retries=3, backoff=1.5):
last_exc = None
for attempt in range(1, max_retries + 1):
try:
timeout = aiohttp.ClientTimeout(total=timeout_sec)
async with aiohttp.ClientSession(timeout=timeout) as sess:
async with sess.post(callback_url, json=payload) as resp:
text = await resp.text()
if 200 <= resp.status < 300:
return {"status": resp.status, "body": text}
else:
last_exc = RuntimeError(f"bad status {resp.status}: {text}")
except Exception as e:
last_exc = e
await asyncio.sleep(backoff ** attempt)
raise last_exc
5) Make transcript save idempotent and fast (Python DB example)
async def save_transcript(transcript_data):
# persist quickly so shutdown hook can re-send callback safely if needed
await db.save_call_transcript(transcript_data)
await db.update_call_status(transcript_data["call_id"], "completed", {
"completed_at": datetime.datetime.now(), "transcript_saved": True
})
6) Node.js (TS) equivalents
// server.ts
import { ServerOptions } from "livekit-agents"; // adjust import as needed
const opts = new ServerOptions({
drainTimeout: 2400, // seconds
// other options...
});
export default defineAgent({
entry: async (ctx) => {
ctx.addShutdownCallback(async () => {
await finalizeTranscriptionAndSendCallback(callId, callbackUrl, transcriptData);
});
// job logic...
},
});
HTTP callback with timeout+retry (Node.js using fetch + abort)
import fetch from "node-fetch";
async function sendCallbackNotification(url, payload, timeoutMs = 5000, retries = 3) {
for (let i = 0; i < retries; i++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
signal: controller.signal,
});
clearTimeout(timer);
if (res.ok) return await res.text();
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text}`);
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * Math.pow(1.5, i)));
}
}
}
Notes / checklist
• Register finalization using ctx.add_shutdown_callback() so the framework runs your hook during session shutdown.
• Use drain_timeout to give jobs time to finish; LiveKit Cloud may have a max rollout window (up to 1 hour).
• Keep callbacks short, add client timeouts, and retries so they don’t hang shutdown.
• Persist transcript first, then send callback so retries are safe.
If you want, tell me which exact file(s) you run (entrypoint name) and I’ll adapt the snippet into your agent’s code shape.
Sources: Deployment reliability on LiveKit Cloud | LiveKit | Deployment reliability on LiveKit Cloud | LiveKit | Job lifecycle | LiveKit Documentation | Server options | LiveKit Documentation | Build a multilingual voice agent that automatically switches languages | Li..., with interactive elementsbulky-byte-27418
04/22/2026, 11:31 AMtall-airline-16756
04/22/2026, 11:31 AMkill -TERM <pid> or press Ctrl+C.
◦ In container/K8s: delete the Pod or scale down so the platform sends SIGTERM.
◦ Simulate rolling deploy if you use LiveKit Cloud (old instance will receive SIGTERM).
1. Observe logs in this order:
◦ Worker becomes unavailable / no new jobs accepted
◦ Active job finishes (or drain begins)
◦ Your shutdown hook log appears (e.g., "Shutdown hook started", "Callback sent")
◦ Session report upload and process exit
If you do not see your hook logs, the hook either didn’t run or was killed mid-run.
1. Shorten drain_timeout for fast tests:
◦ Set a small drain_timeout on your local AgentServer to speed iterations (e.g., 60s) so you can test behavior quickly.
1. Verify callback attempt details:
◦ Log callback request/response and errors inside send_callback_notification so you can tell whether the HTTP call was attempted and if it timed out or failed.
Quick checklist of common issues to check while testing
• Use exec in startup scripts so signals reach the process: exec python agent.py.
• Ensure shutdown hook is registered with ctx.add_shutdown_callback(...) and all async tasks are awaited.
• Give shutdown hooks enough time: increase shutdown_process_timeout and/or drain_timeout for tests.
• Make the callback idempotent and persist transcript before attempting callback so retries are safe.
• Add HTTP timeouts/retries to avoid hangs during shutdown.
If you want, I can:
• Provide a small test entrypoint you can run locally that logs each stage (enter, job end, shutdown hook start/finish), or
• Generate a short K8s manifest showing how to send SIGTERM via Pod deletion for a real-world test.
Which would you like me to give you next?
Sources: Deployment reliability on LiveKit Cloud | LiveKit | Deployment reliability on LiveKit Cloud | LiveKit | Server startup modes | LiveKit Documentation | Job lifecycle | LiveKit Documentation | Why is my Agent insights data missing? | LiveKit, with interactive elements