<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Moisi Trungu</title>
    <description>The latest articles on DEV Community by Moisi Trungu (@moisi_trungu_31647b7ac300).</description>
    <link>https://dev.to/moisi_trungu_31647b7ac300</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2016431%2Fe664cdc4-b987-4a9a-a206-4875fba0b376.png</url>
      <title>DEV Community: Moisi Trungu</title>
      <link>https://dev.to/moisi_trungu_31647b7ac300</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moisi_trungu_31647b7ac300"/>
    <language>en</language>
    <item>
      <title>VoIP Noise Suppression for Call Centers: RNNoise Setup</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/voip-noise-suppression-for-call-centers-rnnoise-setup-209n</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/voip-noise-suppression-for-call-centers-rnnoise-setup-209n</guid>
      <description>&lt;h1&gt;
  
  
  VoIP Noise Suppression for Call Centers: RNNoise Setup
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Background noise, echo, and DTMF artifacts plague inbound and outbound calls in ViciDial: implement RNNoise preprocessing to restore call clarity without codec changes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Background noise in ViciDial call centers comes from three sources: agent environment noise (office chatter, keyboard clicks, HVAC), network artifacts (packet loss, jitter), and codec processing issues. RNNoise, a real-time neural network noise suppressor developed by Xiph.Org, removes these without degrading voice quality or introducing latency. Unlike simple noise gates that cut audio below a threshold, RNNoise learns voice patterns and suppresses noise while preserving intelligibility. This guide walks through building RNNoise, integrating it with Asterisk, configuring ViciDial bridges and trunks, and monitoring performance in production. Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, and CentOS 7/8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need root or sudo access on the ViciDial server. Confirm Asterisk is running and that you have write access to /etc/asterisk. The build requires gcc, git, make, and development headers for Asterisk. Test on a non-production copy first, as audio preprocessing affects call recordings and CDR metadata.&lt;/p&gt;

&lt;p&gt;Check your current setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show version"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module show like codec"&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /etc/asterisk/ | &lt;span class="nb"&gt;grep &lt;/span&gt;sip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the ViciDial database connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; cron &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM vicidial_carrier_log LIMIT 1;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this fails, RNNoise setup will not affect carrier routing, but agent-side noise suppression will still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RNNoise does and why ViciDial agents need it
&lt;/h2&gt;

&lt;p&gt;RNNoise runs a Gated Recurrent Unit (GRU) neural network on incoming audio. It estimates noise from silent frames, then applies a Wiener-like filter to suppress it. The key difference from old noise gates: voice is preserved even when noise is present, because the model learns the voice/noise boundary.&lt;/p&gt;

&lt;p&gt;In a ViciDial call center, noise suppression happens at three points: inbound (customer to agent), outbound (agent to customer), and recording (both directions on disk). Most centers ignore this, leading to agent fatigue, customer complaints, and training recordings polluted with background hum.&lt;/p&gt;

&lt;p&gt;RNNoise runs on the Asterisk bridge, not on the codec. This means you do not re-encode audio or add CPU cost to transcoding. You insert a filter into the audio pipeline before it hits the VoIP endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building RNNoise from source
&lt;/h2&gt;

&lt;p&gt;Clone the RNNoise repository and compile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp
git clone https://gitlab.xiph.org/xiph/rnnoise.git
&lt;span class="nb"&gt;cd &lt;/span&gt;rnnoise
./autogen.sh
./configure &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local
make
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the library installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /usr/local/lib/librnnoise.so&lt;span class="k"&gt;*&lt;/span&gt;
/usr/local/lib/librnnoise.so.0 -&amp;gt; librnnoise.so.0.4.1
ldd /usr/local/lib/librnnoise.so.0 | &lt;span class="nb"&gt;grep &lt;/span&gt;libc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;autogen.sh&lt;/code&gt; fails, install autoconf and automake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; autoconf automake libtool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the Asterisk RNNoise module
&lt;/h2&gt;

&lt;p&gt;Asterisk does not ship with a built-in RNNoise module. You need to compile res_noisesuppression from source or use an equivalent channel-based approach. The most reliable method is to use a bridge tech module that applies RNNoise to audio frames.&lt;/p&gt;

&lt;p&gt;Option 1: Use rnnoise.c bridge filter (simplest for ViciDial)&lt;/p&gt;

&lt;p&gt;Download or create the Asterisk RNNoise module. A community version exists at &lt;a href="https://github.com/BelledonneCommunications/asterisk-rnnoise" rel="noopener noreferrer"&gt;https://github.com/BelledonneCommunications/asterisk-rnnoise&lt;/a&gt; or you can build a minimal version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp
git clone https://github.com/BelledonneCommunications/asterisk-rnnoise.git
&lt;span class="nb"&gt;cd &lt;/span&gt;asterisk-rnnoise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that repo is unavailable, download the source from a working fork:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-O&lt;/span&gt; rnnoise_asterisk.tar.gz &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/asterisk/asterisk-rnnoise/archive/refs/heads/master.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf rnnoise_asterisk.tar.gz
&lt;span class="nb"&gt;cd &lt;/span&gt;asterisk-rnnoise-master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the module finds the RNNoise header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"rnnoise.h"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;res_noisesuppression.c | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the Makefile to point to your RNNoise install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"RNNOISE"&lt;/span&gt; Makefile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Makefile does not exist, create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;RNNOISE_CFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt;/usr/local/include
&lt;span class="nv"&gt;RNNOISE_LIBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt;/usr/local/lib &lt;span class="nt"&gt;-lrnnoise&lt;/span&gt;
&lt;span class="nv"&gt;ASTERISK_CFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;shell asterisk-config &lt;span class="nt"&gt;--cflags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ASTERISK_LIBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;shell asterisk-config &lt;span class="nt"&gt;--libs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;res_noisesuppression.so&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;res_noisesuppression.c&lt;/span&gt;
    gcc &lt;span class="p"&gt;$(&lt;/span&gt;ASTERISK_CFLAGS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;RNNOISE_CFLAGS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-fPIC&lt;/span&gt; &lt;span class="nt"&gt;-shared&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;$@&lt;/span&gt; &lt;span class="nv"&gt;$&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;ASTERISK_LIBS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;RNNOISE_LIBS&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nl"&gt;install&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;res_noisesuppression.so&lt;/span&gt;
    &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nv"&gt;$&amp;lt;&lt;/span&gt; /usr/lib/asterisk/modules/

&lt;span class="nl"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.so &lt;span class="k"&gt;*&lt;/span&gt;.o
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile and install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make clean
make
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /usr/lib/asterisk/modules/res_noisesuppression.so
file /usr/lib/asterisk/modules/res_noisesuppression.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If linking fails, check the library path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ldd /usr/lib/asterisk/modules/res_noisesuppression.so | &lt;span class="nb"&gt;grep &lt;/span&gt;rnnoise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If rnnoise is not found, update the library cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ldconfig
ldd /usr/lib/asterisk/modules/res_noisesuppression.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option 2: Use LuaRocks and Lua filtering (if C compilation fails)&lt;/p&gt;

&lt;p&gt;Some ViciDial setups use Lua for custom features. You can install RNNoise via LuaRocks and apply it in the dialplan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;luarocks &lt;span class="nb"&gt;install &lt;/span&gt;rnnoise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is slower than a native C module but works on locked-down systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Asterisk to load RNNoise
&lt;/h2&gt;

&lt;p&gt;Add the module to the Asterisk startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/asterisk/modules.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;[modules]&lt;/code&gt; section, ensure autoload is enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;modules&lt;/span&gt;]
&lt;span class="n"&gt;autoload&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specifically load the noise suppression module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;preload&lt;/span&gt; = &lt;span class="n"&gt;res_noisesuppression&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt;
&lt;span class="n"&gt;load&lt;/span&gt; = &lt;span class="n"&gt;res_noisesuppression&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart asterisk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the module loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module show like noisesuppression"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Module                         Description                      Use Count
res_noisesuppression.so        RNNoise Audio Processor              0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it does not load, check the Asterisk logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; nois
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common errors: missing librnnoise.so (add /usr/local/lib to ld.so.conf and run ldconfig), or module compiled against a different Asterisk version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying RNNoise to ViciDial bridges
&lt;/h2&gt;

&lt;p&gt;RNNoise is most effective when applied symmetrically: suppress noise from both the agent side and the customer side. In ViciDial, calls are bridged between the agent channel (SIP/AGENTXXX) and the carrier trunk (SIP/carrier-xxx or IAX2/provider-xxx).&lt;/p&gt;

&lt;p&gt;Edit your dialplan extensions to apply RNNoise before the bridge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/asterisk/extensions-vicidial.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the section where the agent and customer are bridged. ViciDial typically does this in the ViciDial context. Add a filter application or bridge option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;ViciDial&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Applying&lt;/span&gt; &lt;span class="n"&gt;RNNoise&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;both&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;noisesuppression&lt;/span&gt;)=&lt;span class="n"&gt;on&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Gosub&lt;/span&gt;(&lt;span class="n"&gt;bridge&lt;/span&gt;-&lt;span class="n"&gt;with&lt;/span&gt;-&lt;span class="n"&gt;noisesuppression&lt;/span&gt;,${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},&lt;span class="m"&gt;1&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()

[&lt;span class="n"&gt;bridge&lt;/span&gt;-&lt;span class="n"&gt;with&lt;/span&gt;-&lt;span class="n"&gt;noisesuppression&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Bridge&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;noise&lt;/span&gt; &lt;span class="n"&gt;suppression&lt;/span&gt; &lt;span class="n"&gt;for&lt;/span&gt; ${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Bridge&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},&lt;span class="n"&gt;b&lt;/span&gt;(&lt;span class="n"&gt;noisesuppression&lt;/span&gt;))
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Return&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the bridge application does not support the &lt;code&gt;b()&lt;/code&gt; parameter, use the generic Bridge with frame hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Bridge&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},,&lt;span class="n"&gt;k&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;k&lt;/code&gt; flag keeps the bridge open for subroutines. Then apply the hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;AddFilter&lt;/span&gt;(&lt;span class="n"&gt;in&lt;/span&gt;,&lt;span class="n"&gt;rnnoise&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;AddFilter&lt;/span&gt;(&lt;span class="n"&gt;out&lt;/span&gt;,&lt;span class="n"&gt;rnnoise&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If AddFilter is not available, check your Asterisk version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show applications"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; filter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Asterisk 13+ supports frame hooks directly in the dialplan. For older versions, apply RNNoise at the SIP channel level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring RNNoise per SIP peer
&lt;/h2&gt;

&lt;p&gt;Instead of applying RNNoise in the dialplan, configure it on the SIP peer definition. This ensures all calls from that peer use suppression.&lt;/p&gt;

&lt;p&gt;Edit the agent SIP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/asterisk/sip-vicidial.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the agent definition (usually &lt;code&gt;[AGENT001]&lt;/code&gt;, &lt;code&gt;[AGENT002]&lt;/code&gt;, etc.). Add the noisesuppression option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;AGENT001&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;device&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="n"&gt;dynamic&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt; = &lt;span class="n"&gt;ViciDial&lt;/span&gt;
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;noisesuppression_settings&lt;/span&gt; = &lt;span class="n"&gt;aggressive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the option is not recognized, your version of Asterisk or the RNNoise module may not support it. Use dialplan-based application instead.&lt;/p&gt;

&lt;p&gt;For inbound carriers, edit the carrier trunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/asterisk/sip-vicidial.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the carrier peer definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;DID_INBOUND&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="n"&gt;carrier&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For IAX2 trunks, edit iax.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/asterisk/iax.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carrier_iax&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="n"&gt;iax&lt;/span&gt;.&lt;span class="n"&gt;carrier&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;username&lt;/span&gt; = &lt;span class="n"&gt;yourusername&lt;/span&gt;
&lt;span class="n"&gt;secret&lt;/span&gt; = &lt;span class="n"&gt;yourpassword&lt;/span&gt;
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the SIP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For IAX2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"iax2 reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peer AGENT001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see noisesuppression listed in the peer details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing RNNoise in a call
&lt;/h2&gt;

&lt;p&gt;Make a test call from an agent phone. Enable verbose logging to see if RNNoise is processing frames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core set verbose 3"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core set debug 2"&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; nois
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the call, you should see messages like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[asterisk] VERBOSE[xxxxx][C-xxxxx]: RNNoise: Processing inbound audio (48000 Hz)
[asterisk] DEBUG[xxxxx][C-xxxxx]: RNNoise: Frame suppressed 12dB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see no RNNoise messages, the module is not active. Check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Module loaded: &lt;code&gt;asterisk -rx "module show like noisesuppression"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Channel has noisesuppression enabled: &lt;code&gt;asterisk -rx "sip show channel SIP/AGENT001-00000001"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dialplan is routing through the RNNoise context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Test recording quality by playing back a call recording:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; /var/spool/asterisk/monitor/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ViciDial stores call recordings in /var/spool/asterisk/monitor/ by default. Listen to a recording before and after enabling RNNoise. The noise floor should drop significantly.&lt;/p&gt;

&lt;p&gt;To automate testing, use a SIP test tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; sipp
sipp &lt;span class="nt"&gt;-sf&lt;/span&gt; uac_scenario.xml &lt;span class="nt"&gt;-s&lt;/span&gt; agent001@your.server.com your.server.com:5060
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why does VociDial show high CPU after enabling RNNoise?
&lt;/h2&gt;

&lt;p&gt;RNNoise runs a small neural network on every audio frame. On a busy system (50+ agents), this can add 5-15% CPU overhead. Monitor with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;top &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pidof asterisk&lt;span class="si"&gt;)&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show brief"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If CPU jumps above 80%, disable RNNoise on low-quality carriers or agent channels that do not need it. Edit sip-vicidial.conf and set noisesuppression = no for specific peers.&lt;/p&gt;

&lt;p&gt;Alternatively, reduce the noisesuppression aggressiveness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;noisesuppression_settings&lt;/span&gt; = &lt;span class="n"&gt;conservative&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RNNoise has three modes: conservative (gentle), balanced (default), and aggressive (maximum suppression). Conservative uses less CPU but leaves more noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do some calls sound robotic after enabling RNNoise?
&lt;/h2&gt;

&lt;p&gt;Overly aggressive RNNoise settings can remove wanted voice frequencies, especially on lower-quality codecs like ulaw or alaw. This happens when the model overestimates noise energy.&lt;/p&gt;

&lt;p&gt;Check your codec settings in /etc/asterisk/sip-vicidial.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;disallow&lt;/span&gt; = &lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt; = &lt;span class="n"&gt;g729&lt;/span&gt;,&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;gsm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using ulaw (North America standard), reduce RNNoise aggressiveness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;noisesuppression_settings&lt;/span&gt; = &lt;span class="n"&gt;conservative&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or switch to a wideband codec like g729 or opus, which preserve more frequency information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt; = &lt;span class="n"&gt;opus&lt;/span&gt;,&lt;span class="n"&gt;g729&lt;/span&gt;,&lt;span class="n"&gt;ulaw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opus especially benefits from RNNoise because it operates at 16 kHz, giving the neural network more spectral detail to work with.&lt;/p&gt;

&lt;p&gt;If the robotic sound persists, check the RNNoise model file. Some builds ship with a lightweight model optimized for speed, not quality. Replace it with the full model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /usr/local/share/rnnoise/rnnoise-demo.model /etc/asterisk/rnnoise.model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit your dialplan to point to the full model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;RNNOISE_MODEL&lt;/span&gt;=/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;rnnoise&lt;/span&gt;.&lt;span class="n"&gt;model&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring RNNoise in production
&lt;/h2&gt;

&lt;p&gt;Log suppression statistics to the database so you can track noise reduction over time. Create a table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vicidial_noisesuppression_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;call_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;agent_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;carrier_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;inbound_suppression_db&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;outbound_suppression_db&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;call_duration&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uniqueid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Lua script to log suppression events. In your dialplan, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;LuaRun&lt;/span&gt;(&lt;span class="n"&gt;log_noise_suppression&lt;/span&gt;.&lt;span class="n"&gt;lua&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lua script (log_noise_suppression.lua):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Log RNNoise suppression metrics&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;io.open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/noisesuppression.fifo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;suppression_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noisesuppression_level&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;string.format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%s|%s|%f&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniqueid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AGENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;suppression_db&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a cron job, read the FIFO and insert into the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/local/bin/log_noisesuppression.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
while read line; do
  call_id=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
  agent_id=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
  suppression=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nt"&gt;-f3&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;

  mysql -u cron -p asterisk -e "INSERT INTO vicidial_noisesuppression_log 
    (call_id, agent_id, inbound_suppression_db, call_date) 
    VALUES ('&lt;/span&gt;&lt;span class="nv"&gt;$call_id&lt;/span&gt;&lt;span class="sh"&gt;', '&lt;/span&gt;&lt;span class="nv"&gt;$agent_id&lt;/span&gt;&lt;span class="sh"&gt;', '&lt;/span&gt;&lt;span class="nv"&gt;$suppression&lt;/span&gt;&lt;span class="sh"&gt;', NOW())"
done &amp;lt; /tmp/noisesuppression.fifo
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/log_noisesuppression.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script in the background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/local/bin/log_noisesuppression.sh &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query suppression trends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbound_suppression_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_suppression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_noisesuppression_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;avg_suppression&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;High suppression on a specific agent indicates a noisy environment. Low suppression indicates good quality or a quiet channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording and CDR impact
&lt;/h2&gt;

&lt;p&gt;RNNoise affects what gets stored on disk. Recordings made after RNNoise processing will have less noise but are not reversible. If your compliance requirements demand original audio, record before RNNoise and after separately.&lt;/p&gt;

&lt;p&gt;Configure recording split in extensions-vicidial.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;MixMonitor&lt;/span&gt;(${&lt;span class="n"&gt;RECORDED_FILE_BEFORE&lt;/span&gt;}.&lt;span class="n"&gt;wav&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;noisesuppression&lt;/span&gt;)=&lt;span class="n"&gt;on&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;MixMonitor&lt;/span&gt;(${&lt;span class="n"&gt;RECORDED_FILE_AFTER&lt;/span&gt;}.&lt;span class="n"&gt;wav&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ViciDial's call_recordings table references the file path. If you create two files, update the vicidial.php agent interface to link both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;call_recordings&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;recording_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xxxxx'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recording_id ties to the vicidial_log uniqueid. One uniqueid, one recording file. To store both, you would need to modify ViciDial's recording logic, which is beyond this scope.&lt;/p&gt;

&lt;p&gt;For most centers, a single RNNoise-processed recording is acceptable and preferred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Module fails to load with "undefined reference to rnnoise_create"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The compiled module cannot find the RNNoise library. Ensure ldconfig has indexed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; /usr/local/lib | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/ld.so.conf.d/rnnoise.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;ldconfig
ldd /usr/lib/asterisk/modules/res_noisesuppression.so | &lt;span class="nb"&gt;grep &lt;/span&gt;rnnoise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If still missing, rebuild the module with explicit LDFLAGS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;LDFLAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-L/usr/local/lib"&lt;/span&gt; make clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Calls drop or become one-way after enabling RNNoise&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RNNoise on one side but not the other causes asymmetry. The side with suppression removes noise, the other side does not. This can confuse Asterisk's echo cancellation.&lt;/p&gt;

&lt;p&gt;Ensure both directions have RNNoise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;AGENT001&lt;/span&gt;]
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;

[&lt;span class="n"&gt;carrier_sip&lt;/span&gt;]
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or apply it symmetrically in the dialplan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;noisesuppression&lt;/span&gt;)=&lt;span class="n"&gt;on&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Bridge&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If calls still drop, disable echo cancellation on the trunk and rely on RNNoise alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carrier_sip&lt;/span&gt;]
&lt;span class="n"&gt;echocancel&lt;/span&gt; = &lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RNNoise is not suppressing anything; verbose log shows no messages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The module may be loaded but not applied to any channels. Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peer AGENT001"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;noisesuppression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output is blank, noisesuppression is not set on that peer. Add it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;AGENT001&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;device&lt;/span&gt;
&lt;span class="n"&gt;noisesuppression&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload and test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make a call and check verbose output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core set verbose 3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If still no output, the module is not recognizing the configuration option. This happens if the module was compiled against a different Asterisk version. Rebuild against your current Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show version"&lt;/span&gt;
make distclean
./configure &lt;span class="nt"&gt;--with-asterisk-includes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/include/asterisk
make
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install
&lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload res_noisesuppression"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Audio is delayed or jittery after RNNoise&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RNNoise adds minimal latency (2-5 ms) per frame. If you hear stuttering, check the audio frame size. Asterisk defaults to 20 ms frames. RNNoise expects consistent frame timing.&lt;/p&gt;

&lt;p&gt;Check Asterisk frame timing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show settings"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; frame
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the frame size is not 20 ms, adjust it in asterisk.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;options&lt;/span&gt;]
&lt;span class="n"&gt;transmit_silence_during_record&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;astmm_audio_frame_size&lt;/span&gt; = &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart asterisk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How much CPU does RNNoise use per call?
&lt;/h3&gt;

&lt;p&gt;RNNoise consumes approximately 1-3% CPU per concurrent call on a modern dual-core processor. On a 50-agent system with 40 concurrent calls, expect 40-120% CPU from RNNoise alone. This scales with codec and agent count. Monitor your system after enabling it on half your agents, then scale up if load remains acceptable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use RNNoise with Cisco or Polycom phones?
&lt;/h3&gt;

&lt;p&gt;RNNoise runs on the Asterisk server, not on the phone. Any SIP phone that works with Asterisk will benefit from RNNoise applied to calls. Phone-side noise cancellation and server-side RNNoise can both be active; they do not conflict. However, if the phone has aggressive echo cancellation, disable it to avoid double-processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does RNNoise work with call recordings?
&lt;/h3&gt;

&lt;p&gt;Yes. Recordings are captured after RNNoise processing, so the suppressed audio is what gets stored. If you need the original unprocessed audio for compliance, record the call before applying RNNoise, or use separate input/output chains in MixMonitor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will RNNoise affect speech recognition or DTMF detection?
&lt;/h3&gt;

&lt;p&gt;RNNoise does not remove or modify DTMF tones because they are distinct frequency pairs. However, poor audio quality before RNNoise can make DTMF harder to detect. RNNoise improves the signal-to-noise ratio, typically helping DTMF detection. Transcription (speech-to-text) benefits from RNNoise because clearer audio trains the model better, but very aggressive RNNoise can remove some voice frequencies that recognition engines rely on. Use the conservative mode if you are doing real-time transcription.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I disable RNNoise on specific calls or carriers?
&lt;/h3&gt;

&lt;p&gt;Yes. Set noisesuppression = no on specific SIP peers in sip-vicidial.conf, or use a dialplan condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;GotoIf&lt;/span&gt;($[&lt;span class="s2"&gt;"${CALLERID(number)}"&lt;/span&gt; = &lt;span class="s2"&gt;"8005551234"&lt;/span&gt;]?&lt;span class="n"&gt;skip_noisesuppression&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;noisesuppression&lt;/span&gt;)=&lt;span class="n"&gt;on&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;(&lt;span class="n"&gt;skip_noisesuppression&lt;/span&gt;):&lt;span class="n"&gt;Bridge&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You have built and installed RNNoise, configured the Asterisk module, applied suppression to agent and carrier channels, and logged suppression metrics to the database. RNNoise is now active on both inbound and outbound calls in your ViciDial system.&lt;/p&gt;

&lt;p&gt;Monitor /var/log/asterisk/messages for errors, run a test call, and listen to a recording before and after to confirm noise reduction. CPU usage should remain below 20% on your dialplan engine. If you notice audio quality degradation, reduce aggressiveness from aggressive to conservative, or switch to a wideband codec like opus.&lt;/p&gt;

&lt;p&gt;In production, check the vicidial_noisesuppression_log table weekly to identify agents or carriers with consistently high noise levels. Noisy agents may need a better microphone or adjusted office settings. Noisy carriers may warrant a provider change.&lt;/p&gt;

&lt;p&gt;Next steps: tune noisesuppression_settings per carrier based on baseline quality, integrate RNNoise metrics into your agent scorecards, and evaluate speech-to-text or quality assurance tools that benefit from cleaner audio.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>Debug SIP call drops: Carrier BYE analysis with Homer</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Sat, 24 Oct 2026 03:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/debug-sip-call-drops-carrier-bye-analysis-with-homer-44om</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/debug-sip-call-drops-carrier-bye-analysis-with-homer-44om</guid>
      <description>&lt;h1&gt;
  
  
  Debug SIP call drops: Carrier BYE analysis with Homer
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Carriers hang up calls mid-conversation with no reason code: identify the SIP BYE trigger and the Q.850 cause that hides it using Homer packet capture and Asterisk logs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SIP call drops during active conversation usually come from carriers sending a BYE request without a proper Q.850 cause code in the SIP headers, or from Asterisk not logging the reason. The fix is to capture the actual SIP packet exchange in Homer, cross-reference it with Asterisk sip.conf debug logs, and then add explicit cause-code mapping in your carrier trunk configuration or a pre-answer check that locks the call state before the carrier can drop it mid-stream.&lt;/p&gt;

&lt;p&gt;Tested on ViciDial 2.14+ (SVN 3555+), Asterisk 16/18/20, MariaDB 10.5+, Homer 7.5+ with SIP capture enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need the following in place before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ViciDial installation with write access to /etc/asterisk and /usr/share/astguiclient&lt;/li&gt;
&lt;li&gt;Asterisk compiled with SIP debugging support (normal ./configure &amp;amp;&amp;amp; make all &amp;amp;&amp;amp; make install)&lt;/li&gt;
&lt;li&gt;Homer SIP capture appliance or Homer agent deployed on the Asterisk box (capture_module installed in Asterisk)&lt;/li&gt;
&lt;li&gt;MySQL/MariaDB command-line access to the asterisk database&lt;/li&gt;
&lt;li&gt;SSH access to the Asterisk server with sudo or root privileges&lt;/li&gt;
&lt;li&gt;A carrier trunk configuration with inbound and outbound calls already active&lt;/li&gt;
&lt;li&gt;At least one reproducible call drop scenario (or a test call scheduled with your carrier)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding SIP BYE and Q.850 cause codes
&lt;/h2&gt;

&lt;p&gt;A BYE request in SIP terminates a call. When a carrier sends a BYE, Asterisk should log the reason in the Reason header (RFC 3326). That header includes a Q.850 cause code, which tells you why the call ended: 16 = normal, 17 = busy, 19 = no answer, 31 = network out of order, 42 = switching equipment congestion, 127 = interworking unspecified.&lt;/p&gt;

&lt;p&gt;In practice, many carriers send a BYE with no Reason header, or send it with cause 127 or 999 (catch-all). When that happens, Asterisk logs "Remote end hung up" or "Hangup requested" with no detail. The call appears in vicidial_log as hangup_cause = '' (empty) or a vague code like NORMAL_UNSPECIFIED. The agent sees silence and has no way to know if the far end disconnected, the trunk dropped, or the carrier's switch hung up first.&lt;/p&gt;

&lt;p&gt;Homer solves this by capturing the actual SIP packet, letting you see the BYE frame, the headers inside it, and the exact timestamp. You then correlate that timestamp with Asterisk's sip.conf debug output and vicidial_log to find the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable SIP packet capture in Homer
&lt;/h2&gt;

&lt;p&gt;Homer needs to be able to listen to your SIP traffic. You have two approaches: run Homer on the Asterisk box itself, or configure Homer to receive a copy of packets from your network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Homer agent on Asterisk
&lt;/h3&gt;

&lt;p&gt;If you are running Homer on a separate server, install the homer-sip-capture module on your Asterisk box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp
wget https://github.com/sipcapture/homer-sip-capture/releases/download/v7.5.0/homer-sip-capture.so
&lt;span class="nb"&gt;cp &lt;/span&gt;homer-sip-capture.so /usr/lib/asterisk/modules/
&lt;span class="nb"&gt;chown &lt;/span&gt;asterisk:asterisk /usr/lib/asterisk/modules/homer-sip-capture.so
&lt;span class="nb"&gt;chmod &lt;/span&gt;755 /usr/lib/asterisk/modules/homer-sip-capture.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then edit /etc/asterisk/modules.conf and ensure the sipkapture module loads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;modules&lt;/span&gt;]
&lt;span class="n"&gt;autoload&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And create /etc/asterisk/homer.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;homer_general&lt;/span&gt;]
&lt;span class="n"&gt;enabled&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;uuid&lt;/span&gt; = &lt;span class="m"&gt;01234567&lt;/span&gt;-&lt;span class="m"&gt;89&lt;/span&gt;&lt;span class="n"&gt;ab&lt;/span&gt;-&lt;span class="n"&gt;cdef&lt;/span&gt;-&lt;span class="m"&gt;0123&lt;/span&gt;-&lt;span class="m"&gt;456789&lt;/span&gt;&lt;span class="n"&gt;abcdef&lt;/span&gt;
&lt;span class="n"&gt;homer_host&lt;/span&gt; = &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;50&lt;/span&gt;
&lt;span class="n"&gt;homer_port&lt;/span&gt; = &lt;span class="m"&gt;9060&lt;/span&gt;
&lt;span class="n"&gt;homer_proto&lt;/span&gt; = &lt;span class="n"&gt;tcp&lt;/span&gt;
&lt;span class="n"&gt;homer_pass&lt;/span&gt; = &lt;span class="n"&gt;homerpass&lt;/span&gt;
&lt;span class="n"&gt;homer_id&lt;/span&gt; = &lt;span class="m"&gt;111&lt;/span&gt;

[&lt;span class="n"&gt;hep_general&lt;/span&gt;]
&lt;span class="n"&gt;enabled&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt; = &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace 192.168.1.50 with your Homer server IP and set homer_pass to match your Homer HEP3 password. Restart Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /etc/init.d/asterisk restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor the logs to confirm the module loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; homer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable SIP debug in Asterisk
&lt;/h3&gt;

&lt;p&gt;In the Asterisk CLI, enable SIP packet logging on the fly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip set debug on"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends all SIP frames (INVITE, 100 Trying, 200 OK, ACK, BYE, 481 Call Leg Does Not Exist) to /var/log/asterisk/messages. The output is verbose but necessary for matching exact timestamps to Homer packets.&lt;/p&gt;

&lt;p&gt;You can limit it to a specific peer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip set debug ip 10.20.30.40"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where 10.20.30.40 is your carrier's gateway IP. This reduces noise in the logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure ViciDial to log call drop reasons
&lt;/h2&gt;

&lt;p&gt;The vicidial_log table stores details of every call. By default, it captures hangup_cause and hangup_cause_id, but ViciDial does not always populate these from the SIP BYE reason. You need to ensure your Asterisk dialplan writes the SIP_HEADER(Reason) to the CDR.&lt;/p&gt;

&lt;p&gt;Edit /etc/asterisk/extensions-vicidial.conf and find the section where inbound calls arrive. Add a line to log the reason before Asterisk hangs up the channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="m"&gt;1234&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CDR&lt;/span&gt;(&lt;span class="n"&gt;user_field&lt;/span&gt;)=${&lt;span class="n"&gt;SIP_HEADER&lt;/span&gt;(&lt;span class="n"&gt;Reason&lt;/span&gt;)})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="m"&gt;1234&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Answer&lt;/span&gt;()
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="m"&gt;1234&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;ViciDial&lt;/span&gt;(${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},${&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;num&lt;/span&gt;)})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stores the raw Reason header value in the CDR user_field column. You can query it later to see what the carrier sent.&lt;/p&gt;

&lt;p&gt;Alternatively, configure your outbound trunk to capture the disconnect code directly. In /etc/asterisk/sip-vicidial.conf, find your carrier trunk definition (e.g., [CARRIER_TRUNK]) and ensure hangup_cause_id is set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;CARRIER_TRUNK&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;trunk&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;20&lt;/span&gt;.&lt;span class="m"&gt;30&lt;/span&gt;.&lt;span class="m"&gt;40&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;=&lt;span class="n"&gt;vicidial_incoming&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;span class="n"&gt;defaultuser&lt;/span&gt;=&lt;span class="n"&gt;vicidial&lt;/span&gt;
&lt;span class="n"&gt;secret&lt;/span&gt;=&lt;span class="n"&gt;your_secret_here&lt;/span&gt;
&lt;span class="n"&gt;insecure&lt;/span&gt;=&lt;span class="n"&gt;port&lt;/span&gt;
&lt;span class="n"&gt;qualify&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;qualifyfreq&lt;/span&gt;=&lt;span class="m"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;hangup_cause_id&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hangup_cause_id parameter tells Asterisk to translate Q.850 codes to Asterisk hang-up causes (NORMAL_CLEARING, BUSY, etc.). This appears in vicidial_log as hangup_cause_id.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture a real call drop and export packets from Homer
&lt;/h2&gt;

&lt;p&gt;Once SIP debug and Homer are running, make or receive a call from the carrier and let it drop. Then go to the Homer web interface (usually &lt;a href="http://192.168.1.50:9080" rel="noopener noreferrer"&gt;http://192.168.1.50:9080&lt;/a&gt;) and search for the call.&lt;/p&gt;

&lt;p&gt;Search by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source IP (your Asterisk box)&lt;/li&gt;
&lt;li&gt;Destination IP (carrier gateway)&lt;/li&gt;
&lt;li&gt;Call-ID (from the vicidial_log entry)&lt;/li&gt;
&lt;li&gt;Time range (around the call's start and end time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click the call in the results. Homer shows a flow diagram of all SIP messages: INVITE, 100, 180, 200 OK, ACK, and finally BYE. Right-click the BYE message and export it as PCAP, or use Homer's built-in SIP message viewer to see the full header.&lt;/p&gt;

&lt;p&gt;Look for the Reason header in the BYE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;BYE sip:10.10.10.10:5060 SIP/2.0
Via: SIP/2.0/UDP 10.20.30.40:5060
From: &amp;lt;sip:1234567@carrier.net&amp;gt;;tag=as1a2b3c
To: &amp;lt;sip:4105551234@10.10.10.10&amp;gt;;tag=0987654
Call-ID: abc123def456@10.20.30.40
CSeq: 102 BYE
Reason: Q.850;cause=127;text="Interworking unspecified"
Content-Length: 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Reason header is missing or cause is 127, 999, or 1, the carrier is not giving you a real reason. This is the root cause of your debug problem.&lt;/p&gt;

&lt;p&gt;If the Reason header exists and shows cause=16 (normal), cause=17 (busy), or another valid code, note it and skip to the Asterisk logs section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-reference with Asterisk sip.conf debug logs
&lt;/h2&gt;

&lt;p&gt;Pull the Asterisk sip.conf debug output for the same call. The logs include the full SIP transaction, including the BYE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 5 &lt;span class="nt"&gt;-B&lt;/span&gt; 5 &lt;span class="s2"&gt;"Call-ID: abc123def456"&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for a line like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;&amp;lt;-- SIP read from UDP:10.20.30.40:5060:
BYE sip:10.10.10.10:5060 SIP/2.0
...
--- (11 headers 0 lines)---
Reason: Q.850;cause=127;text="Interworking unspecified"
Sending to 10.20.30.40:5060 (no NAT)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the BYE shows in Asterisk's logs but no Reason header, the carrier is truly omitting it. If the Reason shows cause 127 or missing, you have two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Contact the carrier and ask them to send a valid Q.850 cause code in the Reason header. Most carriers can do this; it is a configuration on their SBC or switch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the carrier won't cooperate, use SIP Profile or dialplan logic to infer the cause from other signals: call duration, RTP timeout, or a 486 response code earlier in the call.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Map carrier BYE causes to ViciDial hangup codes
&lt;/h2&gt;

&lt;p&gt;ViciDial's hangup_cause_id field maps to Asterisk's standard hangup causes. When a BYE arrives with a Q.850 code, Asterisk converts it to one of these values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;16 (Q.850 normal) -&amp;gt; NORMAL_CLEARING&lt;/li&gt;
&lt;li&gt;17 (Q.850 busy) -&amp;gt; USER_BUSY&lt;/li&gt;
&lt;li&gt;18 (Q.850 no user response) -&amp;gt; NO_ANSWER&lt;/li&gt;
&lt;li&gt;19 (Q.850 no route to destination) -&amp;gt; UNREGISTERED&lt;/li&gt;
&lt;li&gt;31 (Q.850 network out of order) -&amp;gt; NETWORK_OUT_OF_ORDER&lt;/li&gt;
&lt;li&gt;42 (Q.850 switching congestion) -&amp;gt; SWITCH_CONGESTION&lt;/li&gt;
&lt;li&gt;127 (Q.850 interworking) -&amp;gt; UNSPECIFIED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To verify Asterisk is logging these correctly, query vicidial_log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hangup_cause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hangup_cause_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;hangup_cause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If hangup_cause_id is empty, Asterisk did not get a Q.850 code. If it is set (e.g., 16 for NORMAL), Asterisk did. Cross-check the hangup_cause_id values against a Q.850 code list. If you see many calls with hangup_cause_id = 127 (interworking), your carrier is not providing detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a custom cause code lookup in ViciDial
&lt;/h2&gt;

&lt;p&gt;ViciDial stores cause codes in the vicidial_carrier_log table. For each carrier, you can log the SIP Reason header and correlate it with call outcomes. Create a script to parse the Asterisk logs and populate a custom field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/parse_sip_reasons.sh&lt;/span&gt;
&lt;span class="c"&gt;# Extract SIP BYE reasons from Asterisk logs and populate vicidial_log&lt;/span&gt;

&lt;span class="nv"&gt;LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/log/asterisk/messages"&lt;/span&gt;
&lt;span class="nv"&gt;TEMP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/sip_reasons.txt"&lt;/span&gt;

&lt;span class="c"&gt;# Extract Call-ID and Reason header from recent BYE requests&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"BYE sip:"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-500&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;line&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;CALL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'Call-ID: \K[^ ]*'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;REASON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 20 &lt;span class="s2"&gt;"Call-ID: &lt;/span&gt;&lt;span class="nv"&gt;$CALL_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Reason:"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CALL_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CALL_ID&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;$REASON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;

&lt;span class="c"&gt;# Insert parsed reasons into vicidial_log.user_field (or a custom column)&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &amp;lt; /dev/stdin &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
UPDATE vicidial_log SET user_field = CONCAT(user_field, '|', '&lt;/span&gt;&lt;span class="nv"&gt;$REASON&lt;/span&gt;&lt;span class="sh"&gt;')
WHERE call_id IN (SELECT DISTINCT SUBSTRING_INDEX(SUBSTRING_INDEX(line, '|', 1), '|', -1) FROM ... );
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a rough sketch. In practice, you would schedule a cron job to run every 5 minutes and update vicidial_log with the parsed Reason headers. This gives your reporting and quality assurance teams a clear view of why calls ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent mid-call BYE drops with call state locking
&lt;/h2&gt;

&lt;p&gt;Some carriers send a BYE mid-conversation due to a network reset or switch malfunction, not because the agent or customer hung up. To reduce the impact, use Asterisk's progress detection to lock the call state once both sides are connected.&lt;/p&gt;

&lt;p&gt;In /etc/asterisk/extensions-vicidial.conf, after the call is answered, set a channel variable that tells Asterisk to ignore stray BYE requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Answer&lt;/span&gt;()
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;hangup_handler_push&lt;/span&gt;)=&lt;span class="n"&gt;hangup_handler&lt;/span&gt;,&lt;span class="n"&gt;s&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;ViciDial&lt;/span&gt;(${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},${&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;num&lt;/span&gt;)})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;hangup_handler&lt;/span&gt;,&lt;span class="n"&gt;s&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Noop&lt;/span&gt;(&lt;span class="n"&gt;Call&lt;/span&gt; &lt;span class="n"&gt;ended&lt;/span&gt;: ${&lt;span class="n"&gt;HANGUP_CAUSE&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;hangup_handler&lt;/span&gt;,&lt;span class="n"&gt;s&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A more robust approach is to use a custom SIP header to track call state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Answer&lt;/span&gt;()
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;SIP_HEADERS&lt;/span&gt;(&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Call&lt;/span&gt;-&lt;span class="n"&gt;State&lt;/span&gt;)=&lt;span class="n"&gt;locked&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/&lt;span class="n"&gt;carrier&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},&lt;span class="m"&gt;120&lt;/span&gt;,&lt;span class="n"&gt;D&lt;/span&gt;(?&lt;span class="c"&gt;#))
&lt;/span&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;inbound&lt;/span&gt;,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SIP_HEADERS set tells the far end (your carrier) that the call state is locked and any further BYE should include a valid Q.850 reason or be rejected. Not all carriers honor this, but it reduces accidental drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Homer shows packets but Asterisk logs are empty
&lt;/h3&gt;

&lt;p&gt;Homer is capturing traffic but Asterisk is not logging it. Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip set debug on"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then generate another call. If logs are still empty, check that the sip module is loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module show like sip"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it says "No modules matched", rebuild Asterisk with SIP support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /usr/src/asterisk-VERSION
./configure &lt;span class="nt"&gt;--with-pjproject-bundled&lt;/span&gt;
make menuselect
&lt;span class="c"&gt;# Ensure SIP/Chan_SIP or SIP/Chan_PJSIP is selected&lt;/span&gt;
make &lt;span class="nt"&gt;-j4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nb"&gt;install
sudo&lt;/span&gt; /etc/init.d/asterisk restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reason header is missing from Homer packets
&lt;/h3&gt;

&lt;p&gt;The carrier SBC is stripping the Reason header before sending to you. Contact the carrier and request that they preserve SIP headers in transit. Ask them to enable "Reason header forwarding" or "Q.850 pass-through" on their SBC.&lt;/p&gt;

&lt;p&gt;In the meantime, infer the drop reason from call state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If call duration is 0-2 seconds: the call was rejected (cause 17, 18, or 19)&lt;/li&gt;
&lt;li&gt;If call duration is 30+ seconds but RTP stops abruptly: network timeout or switch crash (cause 31 or 42)&lt;/li&gt;
&lt;li&gt;If the agent is still on the call but the customer hangs up: normal (cause 16)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ViciDial shows all calls with empty hangup_cause
&lt;/h3&gt;

&lt;p&gt;Asterisk is receiving BYE but not translating the Q.850 code. Verify hangup_cause_id in your sip.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; hangup_cause_id /etc/asterisk/sip.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is not set, add it to your carrier trunk definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;CARRIER_TRUNK&lt;/span&gt;]
...
&lt;span class="n"&gt;hangup_cause_id&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the SIP config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run a test call and check vicidial_log again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Homer agent is not sending packets to the Homer server
&lt;/h3&gt;

&lt;p&gt;Verify the HEP3 connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"hepstats"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows HEP3 packet counts. If the count is 0, check /etc/asterisk/homer.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/asterisk/homer.conf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^enabled|^homer_host|^homer_port"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure the Homer server is reachable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnet 192.168.1.50 9060
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the connection times out, add a firewall rule or adjust the Homer server's listening address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Call appears in Homer but not in vicidial_log
&lt;/h3&gt;

&lt;p&gt;The call may have failed before it reached the ViciDial dialplan. Check the Asterisk logs for a rejection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"call rejected&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;not found&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;no matching context"&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the DID in your sip.conf context matches the called number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 10 &lt;span class="s2"&gt;"context=vicidial_incoming"&lt;/span&gt; /etc/asterisk/sip.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And check /etc/asterisk/extensions-vicidial.conf for the matching exten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"exten =&amp;gt; _&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/asterisk/extensions-vicidial.conf | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does Homer show a BYE but Asterisk never logs a hangup cause?
&lt;/h3&gt;

&lt;p&gt;Asterisk receives the BYE and hangs up the channel, but the hangup cause is only logged if Asterisk's SIP debug is enabled at the moment the BYE arrives. If you enable SIP debug after the call starts, you may not capture the BYE. Also, some versions of Asterisk do not write the Reason header to the CDR unless it is explicitly captured. Enable SIP debug before initiating the test call, or use Asterisk's CDR driver to force logging of all disconnect reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I force the carrier to send a Q.850 cause code even if they do not normally?
&lt;/h3&gt;

&lt;p&gt;Contact your carrier's technical team and ask them to enable "SIP Reason header preservation" or "Q.850 interworking" on their SBC. Most modern carriers support this. If they refuse, you can configure Asterisk to infer the cause from other signals: call duration, RTP flow, or the SIP response code sent earlier in the call (e.g., 486 Busy, 503 Unavailable).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent calls from being cut off mid-conversation if the carrier sends a spurious BYE?
&lt;/h3&gt;

&lt;p&gt;Use Asterisk's SIP session timer (Session-Expires header) to ensure both ends refresh the session periodically. Add to your carrier trunk in sip.conf: session_expires = 1800, session_minse = 900. This forces the far end to re-INVITE every 30 minutes, which tears down stale calls and prevents zombie sessions. You can also configure a hangup handler that logs the BYE and re-establishes the call if the hangup cause is NETWORK_OUT_OF_ORDER or similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do I find the Q.850 cause codes in my Asterisk logs?
&lt;/h3&gt;

&lt;p&gt;Search for "Reason:" in /var/log/asterisk/messages: grep "Reason:" /var/log/asterisk/messages. Extract the cause= value from the line. Compare it to the Q.850 table: 16 = normal, 17 = busy, 19 = no answer, 21 = call rejected, 31 = network failure, 42 = congestion. If cause is missing or 127, the carrier is not providing detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I export the Homer call data to a CSV for analysis?
&lt;/h3&gt;

&lt;p&gt;Log in to Homer's web interface, run a search, and click the Export button (usually a down arrow icon). Select CSV format. Homer exports a list of all SIP messages in the call with timestamps, source/dest IP, SIP method, and response codes. Open in Excel or import to a database. You can then correlate the Homer data with vicidial_log by Call-ID to map SIP events to ViciDial call records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;SIP call drops with missing or vague Q.850 cause codes are usually caused by carriers stripping Reason headers or not populating them at all. Enable Homer SIP capture and Asterisk sip.conf debug on your carrier trunk, then initiate a test call that drops. Extract the BYE packet from Homer, note whether the Reason header is present and what cause code it contains. Cross-check the timestamp and Call-ID in Asterisk's /var/log/asterisk/messages logs. If the Reason header is missing, request that your carrier enable Q.850 pass-through on their SBC. If it is present, map the cause code to Asterisk's hangup_cause_id in vicidial_log and update your dialplan to log it for reporting. Verify that hangup_cause_id=yes is set in your carrier trunk definition in sip.conf. Next, query vicidial_log to confirm that hangup_cause_id is now populated on all inbound calls, and use that data to identify patterns of why calls are dropping at your carrier's edge.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>VoIP Call Quality Troubleshooting: Smokeping &amp; RTCP Analysis</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/voip-call-quality-troubleshooting-smokeping-rtcp-analysis-2bj0</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/voip-call-quality-troubleshooting-smokeping-rtcp-analysis-2bj0</guid>
      <description>&lt;h1&gt;
  
  
  VoIP Call Quality Troubleshooting: Smokeping &amp;amp; RTCP Analysis
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Diagnose packet loss, jitter, and latency causing one-way audio and dropped calls using Smokeping graphs and RTCP statistics parsed from Asterisk logs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;VoIP call quality degrades when latency exceeds 150ms, jitter spikes above 30ms, or packet loss hits 1% or higher. Smokeping detects these conditions in real time by sending ICMP probes to carriers and gateways; RTCP (Real-time Transport Control Protocol) statistics logged by Asterisk show the actual audio path performance on each call. Together, they isolate whether the problem is network-wide, carrier-specific, or codec-related. Fix the cause by tuning SIP timeouts, adjusting Asterisk jitter buffer settings, or switching carriers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, Smokeping 2.6.11+, and CentOS 7.x.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Root or sudo access to Asterisk/ViciDial server.&lt;/li&gt;
&lt;li&gt;SNMP installed and running (for Smokeping probes).&lt;/li&gt;
&lt;li&gt;Asterisk compiled with RTCP stats enabled (default in most packages).&lt;/li&gt;
&lt;li&gt;Network access to carrier gateways and upstream routers.&lt;/li&gt;
&lt;li&gt;MariaDB client access to the asterisk database.&lt;/li&gt;
&lt;li&gt;Basic knowledge of Asterisk CLI commands and SIP configuration.&lt;/li&gt;
&lt;li&gt;ViciDial web admin panel installed at /vicidial/admin.php.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why does call quality fail on some carriers but not others?
&lt;/h2&gt;

&lt;p&gt;Each carrier's SIP trunk has its own path through the internet: different routers, different buffer depths, different packet scheduling. If your ISP routes calls to Carrier A via a congested edge, but Carrier B through a cleaner path, one will have jitter and loss while the other is clean. Smokeping measures ICMP latency to each gateway; RTCP stats measure actual RTP audio packets on that carrier's calls. When the two disagree, the problem is SIP signaling or call routing, not the network path itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smokeping setup for VoIP monitoring
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installing and configuring Smokeping
&lt;/h3&gt;

&lt;p&gt;Install Smokeping on a dedicated monitoring host or the Asterisk server itself (if CPU headroom allows).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;smokeping fping &lt;span class="nt"&gt;-y&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;smokeping
systemctl start smokeping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the main Smokeping config at /etc/smokeping/config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;*** &lt;span class="n"&gt;Targets&lt;/span&gt; ***
&lt;span class="n"&gt;probe&lt;/span&gt; = &lt;span class="n"&gt;FPing&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;VoIP&lt;/span&gt; &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;Gateways&lt;/span&gt;

+ &lt;span class="n"&gt;Carriers&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;Carriers&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;Upstream&lt;/span&gt; &lt;span class="n"&gt;SIP&lt;/span&gt; &lt;span class="n"&gt;Carriers&lt;/span&gt;

++ &lt;span class="n"&gt;Carrier_A&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;SIP&lt;/span&gt; &lt;span class="n"&gt;Gateway&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;45&lt;/span&gt;
+ &lt;span class="n"&gt;Carrier_B&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="n"&gt;SIP&lt;/span&gt; &lt;span class="n"&gt;Gateway&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="m"&gt;198&lt;/span&gt;.&lt;span class="m"&gt;51&lt;/span&gt;.&lt;span class="m"&gt;100&lt;/span&gt;.&lt;span class="m"&gt;22&lt;/span&gt;

+ &lt;span class="n"&gt;Internal&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;Internal&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;Internal&lt;/span&gt; &lt;span class="n"&gt;Gateways&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;Interfaces&lt;/span&gt;

++ &lt;span class="n"&gt;PBX_Main&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;PBX&lt;/span&gt; &lt;span class="n"&gt;Main&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;Main&lt;/span&gt; &lt;span class="n"&gt;Asterisk&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt;

++ &lt;span class="n"&gt;WAN_Interface&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;WAN&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;WAN&lt;/span&gt; &lt;span class="n"&gt;Uplink&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;2&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Smokeping sends ICMP pings every 60 seconds by default. Set per-target intervals shorter for critical carriers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;++ &lt;span class="n"&gt;Carrier_A&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt; = &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt; = &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;SIP&lt;/span&gt; &lt;span class="n"&gt;Gateway&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;45&lt;/span&gt;
&lt;span class="n"&gt;step&lt;/span&gt; = &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;pings&lt;/span&gt; = &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable cgi and web serving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chown &lt;/span&gt;smokeping:smokeping /var/lib/smokeping/
&lt;span class="nb"&gt;chown &lt;/span&gt;smokeping:smokeping /var/cache/smokeping/
systemctl restart smokeping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access the dashboard at &lt;code&gt;http://your-server/smokeping/&lt;/code&gt; (may require Apache alias configuration).&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Smokeping graphs
&lt;/h3&gt;

&lt;p&gt;The Smokeping graph for each target shows five metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Green line (median latency): actual round-trip time in milliseconds.&lt;/li&gt;
&lt;li&gt;Blue area (50th percentile band): half of your probes fall here.&lt;/li&gt;
&lt;li&gt;Red area (95th percentile band): outlier probes (bad luck, retransmits).&lt;/li&gt;
&lt;li&gt;Gray zones at the bottom: packet loss shown as gaps or light fill.&lt;/li&gt;
&lt;li&gt;Spike up: jitter or a delayed single probe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a carrier's median latency creeps from 45ms to 120ms over an hour, and packet loss appears as gray bands, you have a congestion event. If the red band widens but median stays stable, you have jitter without sustained loss.&lt;/p&gt;

&lt;p&gt;Baseline your carriers under normal call load. Morning, midday, and evening should each show similar latency. If evening spikes 50ms higher than morning, your ISP or carrier oversubscribes evenings.&lt;/p&gt;

&lt;h2&gt;
  
  
  RTCP statistics: what they tell you
&lt;/h2&gt;

&lt;p&gt;RTCP packets flow in parallel with RTP audio. Every few seconds, each end sends stats: bytes sent, packets sent, lost packets, jitter in milliseconds, and round-trip delay. Asterisk logs these in /var/log/asterisk/messages under the RTCP tag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling RTCP logging in Asterisk
&lt;/h3&gt;

&lt;p&gt;Edit /etc/asterisk/sip.conf or sip-vicidial.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;rtcpinterval&lt;/span&gt;=&lt;span class="m"&gt;5000&lt;/span&gt;
&lt;span class="n"&gt;rtcpunit&lt;/span&gt;=&lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload chan_sip.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify RTCP is flowing on an active call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channels verbose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SIP/carrier-a-00000c7a (to caller) (format=ULAW)
  Relaying RTP to SIP/agent-00000c79 (format=ULAW)
  RTCP sent to: 203.0.113.45:16889
  RTCP received from: 203.0.113.45:16888
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parsing RTCP from logs
&lt;/h3&gt;

&lt;p&gt;After a call ends, grep the call ID in /var/log/asterisk/messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"RTCP"&lt;/span&gt; /var/log/asterisk/messages | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical RTCP log line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2024-01-15 14:22:33] VERBOSE[12345][C-0000c7a]: chan_sip.c: RTCP SR (Sender Report) from 203.0.113.45:16888: ssrc=3456789 packets=1245 octets=198400 ntp=e0d1f52a.8b2c3d41 ts=987654 jitter=8.5 loss=0 delay=45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extract and store these metrics in a script. A practical approach: parse the CDR and pull RTCP stats into a custom table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a VoIP quality metrics table
&lt;/h3&gt;

&lt;p&gt;In the asterisk database, add a table for call-level RTCP stats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`voip_call_quality`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;`id`&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`call_date`&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`call_id`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;`carrier`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;`src_ip`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;`dst_ip`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;`jitter_ms`&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`packet_loss_pct`&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`latency_ms`&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`codec`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;`direction`&lt;/span&gt; &lt;span class="nb"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'inbound'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'outbound'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nv"&gt;`call_status`&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="nv"&gt;`call_date_idx`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`call_date`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="nv"&gt;`carrier_idx`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`carrier`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write a PHP script to parse Asterisk logs and insert rows. Run it hourly via cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// /usr/local/bin/parse_rtcp.php&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mysql:host=localhost;dbname=asterisk'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'root'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$logfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/asterisk/messages'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logfile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lines&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'RTCP'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Extract fields using regex&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\].*RTCP.*from\s([\d\.]+).*jitter=([\d.]+)\s.*loss=([\d]+)\s.*delay=(\d+)/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$call_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$src_ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'INSERT INTO voip_call_quality (call_date, src_ip, jitter_ms, packet_loss_pct, latency_ms) VALUES (?, ?, ?, ?, ?)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$call_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$src_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$jitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$loss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$delay&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query the table to spot trends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_jitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet_loss_pct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_loss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;call_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;voip_call_quality&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;avg_loss&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;High loss or jitter on a specific carrier shows up immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common call quality problems and fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One-way audio: caller hears agent, agent hears nothing
&lt;/h3&gt;

&lt;p&gt;One-way audio almost always means the RTP stream from agent to caller is blocked or misrouted. Check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SIP ALG (Application Layer Gateway) is enabled on your router.&lt;/strong&gt; NAT gateways sometimes "help" by rewriting SIP headers, which breaks RTP routing. Disable SIP ALG in your router's advanced settings, or add a static NAT rule:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Router: Port Forward SIP traffic (UDP 5060, 5061) to Asterisk IP
Do NOT enable SIP Proxy or SIP ALG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agent's IP is different from ViciDial's configured IP.&lt;/strong&gt; If your agent is behind a different NAT or carrier than the main Asterisk server, SIP can establish but RTP gets stuck. In /etc/asterisk/sip-vicidial.conf, set:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;externip&lt;/span&gt; = &lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;externhost&lt;/span&gt; = &lt;span class="n"&gt;pbx&lt;/span&gt;.&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Firewall blocking RTP return traffic (ephemeral ports).&lt;/strong&gt; RTP uses high-numbered UDP ports (10000-20000 by default). Allow all outbound UDP from your Asterisk server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On CentOS 7 with firewalld&lt;/span&gt;
firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-rich-rule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'rule family=ipv4 source address=192.168.1.0/24 protocol value=udp port protocol=udp port=10000-20000 accept'&lt;/span&gt;
firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Jitter buffer misconfigured.&lt;/strong&gt; If jitter on the agent side exceeds 50ms, the jitter buffer may be filling then draining, causing audio drop. In extensions-vicidial.conf, find the agent channel definition and set:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;agent&lt;/span&gt;-&lt;span class="n"&gt;channel&lt;/span&gt;]
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;JITTERBUFFER&lt;/span&gt;(&lt;span class="n"&gt;adaptive&lt;/span&gt;)=&lt;span class="n"&gt;yes&lt;/span&gt;)
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;JITTERBUFFER&lt;/span&gt;(&lt;span class="n"&gt;maxsize&lt;/span&gt;)=&lt;span class="m"&gt;200&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;200ms is the max buffer; adaptive mode expands and contracts based on network conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-way audio on specific carriers only
&lt;/h3&gt;

&lt;p&gt;Your carrier's SIP server may not be sending RTP back on the port you sent from. This is a carrier misconfiguration, not your network.&lt;/p&gt;

&lt;p&gt;Check your RTCP logs for that carrier. If you see jitter and loss on one leg but not the other, the issue is asymmetric routing: outbound RTP takes one path, inbound takes another. Carriers often load-balance across multiple gateways.&lt;/p&gt;

&lt;p&gt;Ask the carrier to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin your SIP session to a single media gateway (no load balancing of RTP).&lt;/li&gt;
&lt;li&gt;Confirm their firewall allows your public IP to send RTP to any port they assign (not just 5060).&lt;/li&gt;
&lt;li&gt;Provide a static IP or whitelist for your SIP trunk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Meanwhile, check your trunk settings in /etc/asterisk/sip-vicidial.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="n"&gt;a&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;45&lt;/span&gt;
&lt;span class="n"&gt;fromuser&lt;/span&gt;=&lt;span class="n"&gt;your_account_id&lt;/span&gt;
&lt;span class="n"&gt;secret&lt;/span&gt;=&lt;span class="n"&gt;your_password&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;
&lt;span class="n"&gt;dtmfmode&lt;/span&gt;=&lt;span class="n"&gt;rfc2833&lt;/span&gt;
&lt;span class="n"&gt;natmode&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;qualify&lt;/span&gt;=&lt;span class="m"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;natmode=yes&lt;/code&gt; to send keepalives and &lt;code&gt;qualify=200&lt;/code&gt; to test each peer every 200ms. Restart SIP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  High jitter causes choppy or robotic audio
&lt;/h3&gt;

&lt;p&gt;Jitter of 20-30ms is normal on internet circuits. Above 50ms, audio quality degrades noticeably. Causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Congestion on your ISP link (Smokeping will show rising latency).&lt;/li&gt;
&lt;li&gt;Carrier overload (check RTCP logs for loss; Smokeping to carrier gateway will show red bands).&lt;/li&gt;
&lt;li&gt;Competing traffic (torrent, backup, video) sharing your uplink.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable QoS (Quality of Service) on your router.&lt;/strong&gt; Prioritize SIP and RTP traffic:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Router -&amp;gt; QoS or Traffic Shaping -&amp;gt; UDP port 5060 (SIP) = High Priority
                                 -&amp;gt; UDP port 10000-20000 (RTP) = High Priority
                                 -&amp;gt; Everything else = Low Priority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reduce jitter buffer in Asterisk.&lt;/strong&gt; Counterintuitive, but a large buffer hides jitter initially then delivers a burst of late packets, causing audio dropout. Set to 50ms max:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;JITTERBUFFER&lt;/span&gt;(&lt;span class="n"&gt;maxsize&lt;/span&gt;)=&lt;span class="m"&gt;50&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Switch to G.729 codec if on metered bandwidth.&lt;/strong&gt; G.729 uses 8 kbps vs ULAW's 64 kbps. Fewer packets means less jitter. But check your licensing: G.729 requires a per-channel license. In sip-vicidial.conf:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check if Smokeping shows your ISP uplink is congested.&lt;/strong&gt; If Smokeping to your WAN gateway shows rising latency and packet loss in evenings, you need more bandwidth. Contact your ISP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Packet loss causes dropped words or silent gaps
&lt;/h3&gt;

&lt;p&gt;Packet loss of 0.5% is noticeable; 1% or higher makes calls unusable. Causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Carrier network issue (Smokeping to their gateway shows loss, RTCP confirms it).&lt;/li&gt;
&lt;li&gt;Your ISP oversubscribed (Smokeping shows loss to your upstream router).&lt;/li&gt;
&lt;li&gt;Firewall or IDS dropping packets (less common, but check syslog for drops).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify loss is not just on one codec.&lt;/strong&gt; Query your quality table:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet_loss_pct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_loss&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;voip_call_quality&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If loss is high on ULAW but low on G.729, the issue may be RTP packet size or MTU mismatch, not the network.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check MTU size.&lt;/strong&gt; SIP and RTP packets may be fragmented if your MTU is below 1500 bytes. Confirm:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;show
&lt;span class="c"&gt;# Look for "mtu 1500"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If MTU is below 1500, increase it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;dev eth0 mtu 1500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make persistent in /etc/sysconfig/network-scripts/ifcfg-eth0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;MTU&lt;/span&gt;=&lt;span class="m"&gt;1500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use FEC (Forward Error Correction) on critical carriers.&lt;/strong&gt; Some carriers offer FEC, which adds redundancy so a single lost packet can be recovered. This increases bandwidth but improves quality on lossy links. Ask your carrier if available.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Switch carriers.&lt;/strong&gt; If Smokeping shows your ISP or the carrier has persistent loss, and both confirm the SLA is being met (they contractually allow that loss), you need a different path. Add a second carrier with a different ISP and load-balance calls between them.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Calls drop after 30 seconds
&lt;/h3&gt;

&lt;p&gt;Asterisk's default SIP session timer is 1800 seconds (30 minutes), but if a SIP proxy or NAT device between you and the carrier times out after 30 seconds, the call will drop. Check your carrier's SIP signaling logs first. Then, in sip-vicidial.conf, enable session timers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;session_timers&lt;/span&gt;=&lt;span class="n"&gt;accept&lt;/span&gt;
&lt;span class="n"&gt;session_expires&lt;/span&gt;=&lt;span class="m"&gt;600&lt;/span&gt;
&lt;span class="n"&gt;min_se&lt;/span&gt;=&lt;span class="m"&gt;90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends a RTCP keepalive every 90 seconds. Restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If drops persist, the issue is likely SIP signaling loss (high latency on SIP requests), not the RTP stream. Query your CDR for calls with short duration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;caller_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;called_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LENGTH_IN_SEC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;LENGTH_IN_SEC&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;LENGTH_IN_SEC&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If most of your short calls are exactly 30 seconds, a NAT timeout is happening. Add a SIP registration keepalive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;register&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;your_account_id&lt;/span&gt;:&lt;span class="n"&gt;your_password&lt;/span&gt;@&lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;45&lt;/span&gt;/&lt;span class="n"&gt;your_account_id&lt;/span&gt;

[&lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="n"&gt;a&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;45&lt;/span&gt;
&lt;span class="n"&gt;fromuser&lt;/span&gt;=&lt;span class="n"&gt;your_account_id&lt;/span&gt;
&lt;span class="n"&gt;secret&lt;/span&gt;=&lt;span class="n"&gt;your_password&lt;/span&gt;
&lt;span class="n"&gt;qualify&lt;/span&gt;=&lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;qualify=60&lt;/code&gt; sends a SIP OPTIONS ping every 60 seconds to keep the NAT binding alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlating Smokeping and RTCP data
&lt;/h2&gt;

&lt;p&gt;The most powerful diagnostic is overlaying Smokeping and RTCP metrics on the same timeline.&lt;/p&gt;

&lt;p&gt;Write a script to pull both datasets for a 24-hour window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/voip_quality_report.sh&lt;/span&gt;

&lt;span class="nv"&gt;CARRIER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;all&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;HOURS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;24&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== SMOKEPING LATENCY (avg, min, max) ==="&lt;/span&gt;
sqlite3 /var/lib/smokeping/data/smokeping.db &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
SELECT datetime(timestamp,'unixepoch') as time, median, loss
FROM smokeping_data
WHERE hostname LIKE '%&lt;/span&gt;&lt;span class="nv"&gt;$CARRIER&lt;/span&gt;&lt;span class="sh"&gt;%'
AND timestamp &amp;gt; strftime('%s', 'now', '-&lt;/span&gt;&lt;span class="nv"&gt;$HOURS&lt;/span&gt;&lt;span class="sh"&gt; hours')
ORDER BY timestamp;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== RTCP METRICS (by carrier) ==="&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
SELECT DATE_FORMAT(call_date, '%Y-%m-%d %H:00') as hour,
       carrier,
       AVG(jitter_ms) as jitter,
       AVG(packet_loss_pct) as loss,
       AVG(latency_ms) as latency,
       COUNT(*) as calls
FROM voip_call_quality
WHERE call_date &amp;gt;= DATE_SUB(NOW(), INTERVAL &lt;/span&gt;&lt;span class="nv"&gt;$HOURS&lt;/span&gt;&lt;span class="sh"&gt; HOUR)
AND (carrier LIKE '%&lt;/span&gt;&lt;span class="nv"&gt;$CARRIER&lt;/span&gt;&lt;span class="sh"&gt;%' OR '&lt;/span&gt;&lt;span class="nv"&gt;$CARRIER&lt;/span&gt;&lt;span class="sh"&gt;' = 'all')
GROUP BY DATE_FORMAT(call_date, '%Y-%m-%d %H:%i'), carrier
ORDER BY call_date DESC;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./voip_quality_report.sh carrier-a 24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for time windows where Smokeping shows rising latency and RTCP shows rising jitter and loss. If they correlate, the problem is the network path. If Smokeping is clean but RTCP is poor, the issue is codec, SIP signaling, or internal Asterisk processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asterisk CLI diagnostics for call quality
&lt;/h2&gt;

&lt;p&gt;While a call is active, pull real-time RTP stats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channels verbose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SIP/carrier-a-00000c7a (to 5551234567)
  Codecs: (ulaw|alaw|g729)
  RTP Stream: 192.168.1.10:12345 &amp;lt;-&amp;gt; 203.0.113.45:16888
  Jitter buffer: adaptive, max 200ms
  Last RTP: 48ms ago
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check for RTP gaps (Last RTP &amp;gt; 2 seconds means audio is frozen):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..10&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channels"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"RTP|Last RTP"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Last RTP keeps incrementing, audio is not flowing. Check firewall and NAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Smokeping shows loss but RTCP shows none
&lt;/h3&gt;

&lt;p&gt;Smokeping sends ICMP (ping) traffic; RTP is UDP. Your firewall or router may allow RTP but rate-limit ICMP. ICMP loss is not proof of RTP loss. But if Smokeping shows sustained ICMP loss (&amp;gt;1% for &amp;gt;10 minutes), your ISP uplink is congested and RTP will suffer next.&lt;/p&gt;

&lt;p&gt;Increase Smokeping pings per cycle to get more accurate loss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;++ &lt;span class="n"&gt;Carrier_A&lt;/span&gt;
&lt;span class="n"&gt;pings&lt;/span&gt; = &lt;span class="m"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And confirm with MTR (My Traceroute) for a sustained, detailed view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mtr &lt;span class="nt"&gt;-c&lt;/span&gt; 1000 203.0.113.45 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; mtr_report.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MTR combines ping and traceroute. If you see loss or latency spike at a specific hop (ASN or ISP), report it to your carrier.&lt;/p&gt;

&lt;h3&gt;
  
  
  RTCP shows jitter but audio is clear
&lt;/h3&gt;

&lt;p&gt;Jitter measurement in RTCP is instantaneous. A spike to 80ms on one packet does not mean audio is bad. Look for sustained jitter &amp;gt;50ms over hundreds of packets. Query your quality table by minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;DATE_FORMAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d %H:%i'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;avg_jitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;max_jitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;voip_call_quality&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;minute&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;avg_jitter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;max_jitter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If spikes are rare and brief, your jitter buffer is absorbing them. If sustained, increase buffer size or reduce codec bitrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smokeping has no data
&lt;/h3&gt;

&lt;p&gt;Smokeping is running but the graphs are empty.&lt;/p&gt;

&lt;p&gt;Check the Smokeping daemon log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-100&lt;/span&gt; /var/log/smokeping/smokeping.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;fping binary is missing or broken.&lt;/strong&gt; Reinstall:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum reinstall fping
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /usr/sbin/fping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Smokeping has no write permission.&lt;/strong&gt; Fix:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; smokeping:smokeping /var/lib/smokeping/
&lt;span class="nb"&gt;chmod &lt;/span&gt;755 /var/lib/smokeping/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Target host is unreachable.&lt;/strong&gt; Verify you can ping it manually:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &lt;span class="nt"&gt;-c&lt;/span&gt; 5 203.0.113.45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If packets are blocked, add the target to your firewall whitelist.&lt;/p&gt;

&lt;h3&gt;
  
  
  ViciDial shows calls as DEAD but RTCP shows no loss
&lt;/h3&gt;

&lt;p&gt;"DEAD" status in the ViciDial log usually means the agent hung up or the carrier disconnected the call, not that the call failed mid-stream.&lt;/p&gt;

&lt;p&gt;Check the call disposition in /vicidial/admin.php (Reports -&amp;gt; Inbound/Outbound Call Log) or query directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;called_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LENGTH_IN_SEC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;disconnect_reason&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;disconnect_reason&lt;/code&gt; is empty or "normal", the agent hung up or the call ended naturally. If it contains "SIP/403" or "SIP/486", the carrier rejected the call. If "timeout", SIP signaling stalled.&lt;/p&gt;

&lt;p&gt;Filter for actual failure calls (short duration + SIP error):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;disconnect_reason&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;LENGTH_IN_SEC&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;disconnect_reason&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'SIP%'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does one agent hear perfect audio while another has jitter on the same call?
&lt;/h3&gt;

&lt;p&gt;Jitter is primarily a function of the path from the remote party back to your agent. If two agents are in the same office but receiving calls on different carriers, or from different geographic regions, their RTP paths will differ. The path from New York to Los Angeles is not the same as New York to London. Check RTCP stats per agent and per carrier; you may find that Agent A (in LA) has low jitter from LA-based callers but high jitter from international calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I prioritize Smokeping latency numbers or RTCP jitter numbers?
&lt;/h3&gt;

&lt;p&gt;Prioritize RTCP jitter because it reflects actual audio packet timing. Smokeping shows the baseline path; RTCP shows how audio packets are actually behaving. If Smokeping is clean but RTCP is noisy, the issue is call-specific: codec, SIP processing, or call routing. If both are high, the network path itself is the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is 50ms latency acceptable for VoIP?
&lt;/h3&gt;

&lt;p&gt;Yes. The ITU-T G.114 recommendation allows up to 150ms one-way latency with no perceptible impact. Below 50ms, users notice no delay. Between 50-150ms, the delay is noticeable but tolerable. Above 150ms, calls feel like a satellite conversation (long pause before response). Your baseline latency should be stable and predictable; random jitter is more harmful than high latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I reduce jitter by switching from SIP to IAX2?
&lt;/h3&gt;

&lt;p&gt;IAX2 (Inter-Asterisk eXchange) is a different VoIP protocol that may have lower jitter on certain network paths because it is more efficient than SIP+RTP. However, carrier support is rare. Most carriers only offer SIP trunks. If your carrier supports IAX2, test it against your current SIP trunk on the same calls. Use the same monitoring: Smokeping and RTCP stats.&lt;/p&gt;

&lt;h3&gt;
  
  
  What packet loss is acceptable?
&lt;/h3&gt;

&lt;p&gt;Zero is ideal. Packet loss of 0.1-0.3% is generally not noticeable to the human ear on a 20ms-sample voice codec. Above 0.5%, most callers notice some clipping or words dropping out. Above 1%, the call is poor. If your RTCP data shows &amp;gt;0.5% loss on any carrier, contact the carrier to investigate or switch to a different carrier/path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Deploy Smokeping to continuously monitor latency, jitter, and loss to each carrier gateway and your uplink. Configure RTCP statistics in sip-vicidial.conf and parse the logs into a database table for trending and correlation. When call quality issues arise, overlay Smokeping graphs (network-level view) with RTCP metrics (call-level view) to determine if the problem is the network path, the carrier, your ISP, or Asterisk configuration.&lt;/p&gt;

&lt;p&gt;Start by establishing baseline metrics for each carrier during normal hours. Document acceptable jitter (&amp;lt;30ms), loss (&amp;lt;0.5%), and latency ranges (&amp;lt;100ms). When problems occur, query the quality table by hour and carrier to identify when the issue started and which carriers or paths are affected.&lt;/p&gt;

&lt;p&gt;Key diagnostics to implement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Smokeping config updated with all carriers and gateways.&lt;/li&gt;
&lt;li&gt;RTCP logging enabled in sip-vicidial.conf and voip_call_quality table created.&lt;/li&gt;
&lt;li&gt;Automated hourly RTCP parsing script running via cron.&lt;/li&gt;
&lt;li&gt;Hourly or daily quality report script comparing Smokeping and RTCP metrics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is your Asterisk server's external IP and NAT settings correct in sip-vicidial.conf.&lt;/li&gt;
&lt;li&gt;Are firewall rules allowing RTP return traffic on high ports (10000-20000).&lt;/li&gt;
&lt;li&gt;Has your QoS configuration prioritized SIP and RTP over bulk data.&lt;/li&gt;
&lt;li&gt;Do you have a second carrier or ISP link to failover to when one degrades.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>Auto QA Call Recording Quality with NISQA &amp; Silero VAD</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:30:03 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/auto-qa-call-recording-quality-with-nisqa-silero-vad-1c3b</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/auto-qa-call-recording-quality-with-nisqa-silero-vad-1c3b</guid>
      <description>&lt;h1&gt;
  
  
  Auto QA Call Recording Quality with NISQA &amp;amp; Silero VAD
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Detect poor call audio in seconds by measuring speech intelligibility and voice activity during ViciDial recording post-processing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Call recording quality problems kill agent training data, compliance audits, and customer experience analysis. NISQA (Non-Intrusive Speech Quality Assessment) scores audio intelligibility on a 1-5 MOS (Mean Opinion Score) scale without needing a reference recording. Silero VAD (Voice Activity Detection) identifies silence, background noise, and clipping. Run both tools against recordings in your ViciDial archive immediately after the call ends. Flag low-quality files, trigger re-recording for critical calls, and alert supervisors to codec or network issues before they compound.&lt;/p&gt;

&lt;p&gt;This guide walks through installation, integration with ViciDial's recording pipeline, real scoring workflows, and troubleshooting. Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, and NISQA 1.0+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need a working ViciDial system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asterisk 16 or later (18 preferred for codec stability)&lt;/li&gt;
&lt;li&gt;MariaDB 10.5+&lt;/li&gt;
&lt;li&gt;Call recordings in .wav or .mp3 format stored in /var/spool/asterisk/monitor/ or equivalent&lt;/li&gt;
&lt;li&gt;Python 3.8+&lt;/li&gt;
&lt;li&gt;Root or sudo access&lt;/li&gt;
&lt;li&gt;Disk space: 500MB minimum for NISQA/Silero models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optional but recommended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supervisor or QA user account in ViciDial&lt;/li&gt;
&lt;li&gt;Grafana or web dashboard for QA metrics&lt;/li&gt;
&lt;li&gt;Cron or systemd timer for automated scoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why recording quality breaks in ViciDial
&lt;/h2&gt;

&lt;p&gt;Call audio quality depends on codec selection, RTP timing, jitter buffers, and network path. A SIP call routed through a poor WAN link with G.729 compression may score 2.0 on NISQA even though the call connected. Agents report audio as "choppy" or "robotic." Without automated scoring, quality issues hide in the archive until QA staff manually spot them.&lt;/p&gt;

&lt;p&gt;NISQA and Silero detect these problems programmatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NISQA scores overall intelligibility (1=bad, 5=excellent)&lt;/li&gt;
&lt;li&gt;Silero VAD measures percentage of active speech, identifies gaps, detects clipping or over-compression&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they flag calls where the agent or customer cannot be understood, or where background noise drowns out speech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install NISQA and Silero VAD
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Download and configure NISQA
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt
git clone https://github.com/gabrielmittag/NISQA.git
&lt;span class="nb"&gt;cd &lt;/span&gt;NISQA
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
pip &lt;span class="nb"&gt;install &lt;/span&gt;torch torchaudio librosa numpy scipy matplotlib
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NISQA downloads a pre-trained model (~200MB) on first run. Test the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/NISQA
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from nisqa.NISQA_model import NISQAModel; m = NISQAModel(pretrained_model_dir='nisqa_pretrained_model'); print('NISQA ready')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Model download runs in the background. On first execution, wait 2-3 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Silero VAD
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;silero-vad
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import torch; torch.hub.load('snakers4/silero-vad', 'get_speech_timestamps', trust_repo=True); print('Silero VAD ready')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silero downloads a ~40MB ONNX model the first time you call it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify codec support
&lt;/h3&gt;

&lt;p&gt;NISQA and Silero work on raw audio streams. If your ViciDial recordings are compressed (G.729, GSM), decode them first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ffmpeg
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; call_20250115_120000_5551234567.wav &lt;span class="nt"&gt;-acodec&lt;/span&gt; pcm_s16le &lt;span class="nt"&gt;-ar&lt;/span&gt; 16000 decoded.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or decode in-process in your scoring script (covered below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the QA scoring workflow
&lt;/h2&gt;

&lt;p&gt;Your workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call ends, asterisk writes .wav or .mp3 to /var/spool/asterisk/monitor/&lt;/li&gt;
&lt;li&gt;ViciDial cron job moves or copies file to archive&lt;/li&gt;
&lt;li&gt;Your QA service picks up the file, decodes, runs NISQA + Silero VAD&lt;/li&gt;
&lt;li&gt;Scores are inserted into a ViciDial database table&lt;/li&gt;
&lt;li&gt;QA dashboard or agent screen shows results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a new table in the asterisk database to hold QA results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;vicidial_qa_scores&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;qa_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;vicidial_log_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;nisqa_score&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;nisqa_raw&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;nisqa_dim_col&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;nisqa_dim_noi&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;nisqa_dim_mos&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;speech_percentage&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;silence_duration_sec&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;noise_floor_db&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;clipping_detected&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'processed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;unique_file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;idx_vicidial_log_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vicidial_log_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;idx_processing_status&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This table links to ViciDial's vicidial_log table via vicidial_log_id. The scoring service queries for recordings not yet scored, processes them, and updates this table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the QA scoring service
&lt;/h2&gt;

&lt;p&gt;Here is a production-grade Python script that monitors your recording directory, scores new files, and stores results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
ViciDial Call Recording QA Scorer using NISQA and Silero VAD
Monitors recording directory, scores audio, updates database
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;librosa&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nisqa.NISQA_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NISQAModel&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s - %(levelname)s - %(message)s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/var/log/asterisk/qa_scorer.log&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QAScorer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asteriskuser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;db_pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asterisk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asterisk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/var/spool/asterisk/monitor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_host&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_pass&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Initialize NISQA model
&lt;/span&gt;        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading NISQA model...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nisqa_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NISQAModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pretrained_model_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/opt/NISQA/nisqa_pretrained_model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Load Silero VAD
&lt;/span&gt;        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading Silero VAD...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;silero_vad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snakers4/silero-vad&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;get_speech_timestamps&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;trust_repo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;silero_get_speech_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snakers4/silero-vad&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;get_speech_timestamps&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;trust_repo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_name&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Database connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Database connection failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_sr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Decode any audio format to 16kHz mono PCM&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.wav&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_sr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;resampler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_sr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;waveform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;waveform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;target_sr&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Decode error &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Use ffmpeg for other formats
&lt;/span&gt;            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;temp_wav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_temp.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ffmpeg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-i&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-ar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_sr&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-ac&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wav&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp_wav&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-y&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
                &lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FFmpeg decode error &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_nisqa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Score audio with NISQA&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# NISQA expects dict input with 'speech' key
&lt;/span&gt;            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nisqa_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_numpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sr&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos_raw&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos_raw&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_col&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_col&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_noi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_noi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_mos&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_mos&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NISQA scoring error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_vad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Score voice activity with Silero VAD&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Silero VAD expects float32 tensor
&lt;/span&gt;            &lt;span class="n"&gt;wav_tensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_numpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wav_tensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;wav_tensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wav_tensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Get speech timestamps (list of dicts with 'start' and 'end' in samples)
&lt;/span&gt;            &lt;span class="n"&gt;speech_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;silero_get_speech_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wav_tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;speech_percentage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;silence_duration_sec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_speech_segments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;clipping_detected&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;noise_floor_db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;total_speech_samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;speech_ts&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;speech_percentage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_speech_samples&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
            &lt;span class="n"&gt;silence_duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total_speech_samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;

            &lt;span class="c1"&gt;# Detect clipping (samples at near max amplitude)
&lt;/span&gt;            &lt;span class="n"&gt;max_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;clipping_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;
            &lt;span class="n"&gt;clipping_detected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_val&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clipping_threshold&lt;/span&gt;

            &lt;span class="c1"&gt;# Estimate noise floor from silent segments
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;silent_regions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;silent_regions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Gaps larger than 50ms
&lt;/span&gt;                        &lt;span class="n"&gt;silent_regions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;silent_regions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;noise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silent_regions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;rms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;noise&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;noise_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rms&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;noise_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;noise_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;80.0&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;speech_percentage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech_percentage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;silence_duration_sec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;silence_duration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_speech_segments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech_ts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;clipping_detected&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clipping_detected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;noise_floor_db&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;noise_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VAD scoring error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_vicidial_log_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extract or look up vicidial_log_id from filename or database&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="c1"&gt;# ViciDial filenames are typically YYYYMMDD-HHMMSS_UNIQUEID.wav
&lt;/span&gt;        &lt;span class="c1"&gt;# Try to find in vicidial_log by matching filename
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT vicidial_log_id FROM vicidial_log
                WHERE recording LIKE CONCAT(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, %s, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
                ORDER BY call_date DESC LIMIT 1
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Query error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Score a single recording file&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

        &lt;span class="c1"&gt;# Check if already processed
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT qa_id FROM vicidial_qa_scores WHERE filename = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skipping &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (already scored)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lookup error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Decode audio
&lt;/span&gt;        &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_qa_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Audio decode failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Run NISQA
&lt;/span&gt;        &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score_nisqa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_qa_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NISQA scoring failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Run Silero VAD
&lt;/span&gt;        &lt;span class="n"&gt;vad_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score_vad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vad_result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_qa_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;VAD scoring failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Insert results
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_qa_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vad_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;processed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Scored &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: NISQA=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Speech=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;vad_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;speech_percentage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_qa_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vad_result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Insert QA scores into database&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;vicidial_log_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_vicidial_log_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO vicidial_qa_scores (
                    filename, vicidial_log_id,
                    nisqa_score, nisqa_raw, nisqa_dim_col, nisqa_dim_noi, nisqa_dim_mos,
                    speech_percentage, silence_duration_sec, clipping_detected,
                    processing_status, error_message
                ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;vicidial_log_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mos_raw&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_col&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_noi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;nisqa_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dim_mos&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nisqa_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;vad_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;speech_percentage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vad_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;vad_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;silence_duration_sec&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vad_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;vad_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;clipping_detected&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vad_result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Insert error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_and_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age_hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Scan recording directory and process unscored files&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recordings dir not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;cutoff_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;wav_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*.wav&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; \
                    &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*.mp3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wav_files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; audio files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wav_files&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ViciDial QA Call Recording Scorer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--db-host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--db-user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asteriskuser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--db-pass&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asterisk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--recordings-dir&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/var/spool/asterisk/monitor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--age-hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Only process files younger than this&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;scorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QAScorer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;db_pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;recordings_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recordings_dir&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scorer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan_and_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age_hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_hours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scorer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QA scoring completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this script to &lt;code&gt;/opt/vicidial_qa_scorer.py&lt;/code&gt; and make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /opt/vicidial_qa_scorer.py
pip &lt;span class="nb"&gt;install &lt;/span&gt;MySQLdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Schedule automated scoring with systemd
&lt;/h2&gt;

&lt;p&gt;Create a systemd timer to run scoring every 5 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/vicidial-qa-scorer.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ViciDial QA Call Recording Scorer&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;asterisk.service mysql.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /opt/vicidial_qa_scorer.py &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--db-host localhost &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--db-user asteriskuser &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--db-pass asterisk &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--recordings-dir /var/spool/asterisk/monitor &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;--age-hours 12&lt;/span&gt;
&lt;span class="py"&gt;StandardOutput&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="py"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/vicidial-qa-scorer.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ViciDial QA Scorer Timer&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;vicidial-qa-scorer.service&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnBootSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2min&lt;/span&gt;
&lt;span class="py"&gt;OnUnitActiveSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5min&lt;/span&gt;
&lt;span class="py"&gt;AccuracySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1min&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start the timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;vicidial-qa-scorer.timer
systemctl start vicidial-qa-scorer.timer
systemctl status vicidial-qa-scorer.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  View and alert on QA scores
&lt;/h2&gt;

&lt;p&gt;Query your scores to find poor-quality calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find calls with low NISQA scores&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nisqa_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speech_percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clipping_detected&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_qa_scores&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;nisqa_score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processed'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Find calls with excessive silence&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speech_percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silence_duration_sec&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_qa_scores&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;speech_percentage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processed'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Find calls with clipping&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nisqa_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clipping_detected&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_qa_scores&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;clipping_detected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processed'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an alert script that emails supervisors when quality drops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /opt/qa_alert.sh - Run hourly via cron&lt;/span&gt;

&lt;span class="nv"&gt;THRESHOLD_MOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.5
&lt;span class="nv"&gt;THRESHOLD_SPEECH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;40.0

&lt;span class="nv"&gt;MYSQL_PASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"asterisk"&lt;/span&gt;
&lt;span class="nv"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"asteriskuser"&lt;/span&gt;
&lt;span class="nv"&gt;MYSQL_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;
&lt;span class="nv"&gt;MYSQL_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"asterisk"&lt;/span&gt;

&lt;span class="nv"&gt;QUERY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SELECT COUNT(*) FROM vicidial_qa_scores
WHERE processing_status = 'processed'
AND call_date &amp;gt; NOW() - INTERVAL 1 HOUR
AND (nisqa_score &amp;lt; &lt;/span&gt;&lt;span class="nv"&gt;$THRESHOLD_MOS&lt;/span&gt;&lt;span class="s2"&gt; OR speech_percentage &amp;lt; &lt;/span&gt;&lt;span class="nv"&gt;$THRESHOLD_SPEECH&lt;/span&gt;&lt;span class="s2"&gt;);"&lt;/span&gt;

&lt;span class="nv"&gt;COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nv"&gt;$MYSQL_HOST&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nv"&gt;$MYSQL_USER&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="nv"&gt;$MYSQL_PASS&lt;/span&gt; &lt;span class="nv"&gt;$MYSQL_DB&lt;/span&gt; &lt;span class="nt"&gt;-se&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUERY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COUNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"QA Alert: &lt;/span&gt;&lt;span class="nv"&gt;$COUNT&lt;/span&gt;&lt;span class="s2"&gt; calls with poor quality in the last hour"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
      mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"ViciDial QA Alert"&lt;/span&gt; supervisor@company.com
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add to /etc/cron.d/vicidial-qa&lt;/span&gt;
0 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; root /opt/qa_alert.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/asterisk/qa_alert.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interpret NISQA scores
&lt;/h2&gt;

&lt;p&gt;NISQA outputs a Mean Opinion Score (MOS) from 1.0 to 5.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.0-1.5: Nearly unintelligible. Artifacts, heavy compression, or noise dominate.&lt;/li&gt;
&lt;li&gt;1.5-2.5: Poor. Speech recognizable but with effort. Typical of congested networks or very compressed codecs.&lt;/li&gt;
&lt;li&gt;2.5-3.5: Fair. Acceptable for most applications but QA should review. Moderate artifacts.&lt;/li&gt;
&lt;li&gt;3.5-4.5: Good. Commercial-grade audio. Suitable for compliance and training.&lt;/li&gt;
&lt;li&gt;4.5-5.0: Excellent. Minimal artifacts, clear speech, no background noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Silero VAD reports speech percentage (0-100%). Below 40% indicates a call that is mostly silent or heavily noise-gated. Above 70% with a high NISQA score is ideal.&lt;/p&gt;

&lt;p&gt;Clipping (amplitude near 1.0 for multiple consecutive samples) indicates gain-staging problems. Reduce input gain on the SIP trunk or agent headset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why NISQA scores vary by codec
&lt;/h2&gt;

&lt;p&gt;ViciDial typically uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ULAW (default): Wide compression, acceptable loss. NISQA 3.2-3.8 on clean networks.&lt;/li&gt;
&lt;li&gt;G.729: Narrow bandwidth, 8kHz, licensed codec. NISQA 2.5-3.2 due to aggressive compression.&lt;/li&gt;
&lt;li&gt;GSM: Mobile codec, poor fidelity. NISQA 2.0-2.8.&lt;/li&gt;
&lt;li&gt;OPUS or Speex: Modern codecs, better fidelity. NISQA 3.8-4.5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agents complain about "robot voices," check your trunk codec settings in /etc/asterisk/sip-vicidial.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carrier_name&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;codec&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;alaw&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch preferred codec order by listing best-quality first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;alaw&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or force one codec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run QA scoring again on the next batch of calls. NISQA scores should improve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle special cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-channel recordings
&lt;/h3&gt;

&lt;p&gt;Some ViciDial setups record agent and customer on separate channels, then mix them. If your wav file is stereo, decode to mono before scoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;waveform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Mix to mono
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python script above already does this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Very short calls
&lt;/h3&gt;

&lt;p&gt;Calls under 3 seconds confuse both NISQA and Silero. Add a check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skipping &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: too short&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_qa_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processing_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;skipped&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Call duration &amp;lt; 3 seconds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Noisy environments
&lt;/h3&gt;

&lt;p&gt;Call centers with open floor plans have high background noise. Silero VAD will report low speech percentage even if the call is successful. Compare your NISQA dim_noi (noise dimension) score with speech percentage. High dim_noi and low speech percentage indicate environmental noise, not transmission loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NISQA model download hangs
&lt;/h3&gt;

&lt;p&gt;The model (200MB) downloads on first inference. If your server has slow or metered internet, pre-download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/NISQA
python3 &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
from nisqa.NISQA_model import NISQAModel
print("Downloading model...")
m = NISQAModel(pretrained_model_dir='nisqa_pretrained_model')
print("Done. Model at nisqa_pretrained_model/")
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can take 5-10 minutes on slow connections. Run it interactively so you see progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  "CUDA out of memory" when scoring
&lt;/h3&gt;

&lt;p&gt;NISQA and Silero VAD load the model into GPU memory. If you have a low-VRAM GPU or many concurrent scores, move models to CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In QAScorer.__init__:
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nisqa_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NISQAModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pretrained_model_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/opt/NISQA/nisqa_pretrained_model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# Force CPU
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scoring will be slower but will not crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Silero VAD failing with "AssertionError: Assert sr == 16000"
&lt;/h3&gt;

&lt;p&gt;Silero VAD is strict about sample rate. Ensure your decode function resamples to 16kHz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resampler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchaudio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;waveform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resampler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MySQL error "Column 'noise_floor_db' doesn't exist"
&lt;/h3&gt;

&lt;p&gt;Your vicidial_qa_scores table is missing the optional column. Add it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;vicidial_qa_scores&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;noise_floor_db&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scoring takes 30+ seconds per file
&lt;/h3&gt;

&lt;p&gt;NISQA inference on CPU takes 15-30 seconds per file. If you have thousands of backlog files, run multiple scorer instances in parallel with different time windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scorer 1: files from 0-6 hours ago&lt;/span&gt;
python3 /opt/vicidial_qa_scorer.py &lt;span class="nt"&gt;--age-hours&lt;/span&gt; 6 &amp;amp;

&lt;span class="c"&gt;# Scorer 2: files from 6-12 hours ago (modify script to support age range)&lt;/span&gt;
python3 /opt/vicidial_qa_scorer.py &lt;span class="nt"&gt;--age-hours&lt;/span&gt; 12 &amp;amp;

&lt;span class="nb"&gt;wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or schedule with staggered cron entries at 5-minute intervals.&lt;/p&gt;

&lt;h3&gt;
  
  
  ffmpeg errors on .mp3 files
&lt;/h3&gt;

&lt;p&gt;If ffmpeg fails on .mp3, install the codec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; libavcodec-extra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or upgrade ffmpeg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ffmpeg
ffmpeg &lt;span class="nt"&gt;-version&lt;/span&gt;  &lt;span class="c"&gt;# Should be 4.2+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scores not appearing in database
&lt;/h3&gt;

&lt;p&gt;Check the service log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; vicidial-qa-scorer.service &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/asterisk/qa_scorer.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database credentials wrong in service file&lt;/li&gt;
&lt;li&gt;Recording directory path not found or not readable&lt;/li&gt;
&lt;li&gt;Permissions: script runs as root, but /var/spool/asterisk/monitor/ may be owned by asterisk user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;755 /var/spool/asterisk/monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Asterisk not recording
&lt;/h3&gt;

&lt;p&gt;Call recordings are disabled if recording is not set in dialplan or extensions-vicidial.conf. Check that MixMonitor is being called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"MixMonitor"&lt;/span&gt; /etc/asterisk/extensions-vicidial.conf | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MixMonitor(/var/spool/asterisk/monitor/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If missing, update your dialplan or ensure ViciDial's call center is configured to record calls (Web admin &amp;gt; Agents &amp;gt; Settings).&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What NISQA score should I use as a threshold for re-recording or escalation?
&lt;/h3&gt;

&lt;p&gt;Use 2.5 as a warning threshold (review the call) and 2.0 as a failure threshold (re-record for compliance or training). Calls below 2.0 are typically unintelligible to humans. Anything above 3.5 is acceptable for all uses. Thresholds depend on your SLA. QA operations often flag 2.5-3.0 for supervisor review and automatically reject 1.5-2.5.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use NISQA without Silero VAD?
&lt;/h3&gt;

&lt;p&gt;Yes. NISQA alone gives you overall speech quality. However, NISQA doesn't distinguish between silence, background noise, and actual speech quality. A call that is 90% silence will score higher than a loud, cl&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>ViciDial Call Distribution: Agent Rank &amp; Load Balancing</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Wed, 21 Oct 2026 03:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/vicidial-call-distribution-agent-rank-load-balancing-3o9l</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/vicidial-call-distribution-agent-rank-load-balancing-3o9l</guid>
      <description>&lt;h1&gt;
  
  
  ViciDial Call Distribution: Agent Rank &amp;amp; Load Balancing
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Uneven agent workload and call abandonment happen when rank distribution is misconfigured: fix queue strategy, agent status routing, and load-balancing weights to match your team size and call volume.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Call distribution in ViciDial depends on three overlapping systems: agent rank (a numeric priority assigned to each agent), queue strategy (how Asterisk selects the next agent), and load balancing (how calls are weighted across groups). When rank is set incorrectly, calls queue up behind low-skill agents while senior agents sit idle, or they distribute randomly instead of fairly. The fix involves setting rank values correctly in the database, choosing the right queue strategy in extensions, and validating agent status flow through the inbound group configuration.&lt;/p&gt;

&lt;p&gt;Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, with SIP trunks and local agent registration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SSH access to the ViciDial server and MySQL command line or phpMyAdmin&lt;/li&gt;
&lt;li&gt;Ability to edit /etc/asterisk/extensions-vicidial.conf and reload Asterisk&lt;/li&gt;
&lt;li&gt;Understanding of ViciDial agent statuses: READY, PAUSED, INCALL, DEAD, RING, DISPO&lt;/li&gt;
&lt;li&gt;At least two test agents configured and logged in&lt;/li&gt;
&lt;li&gt;Sample inbound DID or campaign with a queue configured&lt;/li&gt;
&lt;li&gt;Permission to run &lt;code&gt;asterisk -rx&lt;/code&gt; commands as root&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding agent rank in ViciDial
&lt;/h2&gt;

&lt;p&gt;Agent rank is a numeric field (0-9999) stored in the vicidial_users table. Lower numbers are called first. Rank is not tied to role or permission; it is purely a call-distribution priority within a specific inbound group.&lt;/p&gt;

&lt;p&gt;Open a terminal and check current rank assignments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT user_id, full_name, user_rank, inbound_group FROM vicidial_users WHERE user_rank IS NOT NULL ORDER BY inbound_group, user_rank;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| user_id | full_name      | user_rank | inbound_group |
|---------|----------------|-----------|---------------|
| 1001    | Alice Senior   | 1         | SALES         |
| 1002    | Bob Support    | 2         | SALES         |
| 1003    | Carol New      | 99        | SALES         |
| 2001    | Dave Tech      | 1         | SUPPORT       |
| 2002    | Eve Tech       | 2         | SUPPORT       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rank 1 agents in SALES get calls first. When Alice is READY, she answers. When she goes PAUSED or INCALL, rank 2 (Bob) becomes the next candidate. Rank 99 (Carol, new) only gets calls if Alice and Bob are not available.&lt;/p&gt;

&lt;p&gt;The inbound_group column links each agent to a phone queue. Agents in different groups do not compete for the same calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up rank for balanced distribution
&lt;/h2&gt;

&lt;p&gt;For a team of five agents handling the same queue, a fair setup assigns ranks 1 through 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1001'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1002'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1003'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1004'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1005'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit the changes. Do not use gaps (rank 1, 3, 5, 10). Asterisk iterates through each rank in order; unused rank values waste cycles.&lt;/p&gt;

&lt;p&gt;For a tiered support model (senior handling overflow):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1001'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1002'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1003'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2001'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the senior agent (rank 1) gets first call, then three junior agents. This ensures skill routing without complex queues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queue strategy and load balancing
&lt;/h2&gt;

&lt;p&gt;Asterisk offers five queue strategies. ViciDial uses them in the extensions-vicidial.conf file. The choice affects how agents are selected at each call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy selection: what works for ViciDial
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ringall&lt;/strong&gt;: Rings all available agents in rank order simultaneously. The first to answer gets the call. Good for small teams (3-5 agents). Higher answer rate, but agents get ring storms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;leastrecent&lt;/strong&gt;: Rings the agent least recently called, then the next least recent, in rank order. Distributes calls more evenly. Recommended for most setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fewest&lt;/strong&gt;: Rings the agent with the fewest current calls. Useful if agents handle multiple calls at once (rare in ViciDial).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;rrmemory&lt;/strong&gt;: Round-robin with memory; cycles through agents in rank order and remembers the last agent called. Fair, stable. Good for equal-skill teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;linear&lt;/strong&gt;: Rings agents one at a time, in strict rank order, waiting for timeout before moving to the next. Slow for high volume. Avoid.&lt;/p&gt;

&lt;p&gt;For a standard ViciDial inbound group with 5-20 agents of similar skill, use &lt;strong&gt;leastrecent&lt;/strong&gt; or &lt;strong&gt;rrmemory&lt;/strong&gt;. For senior + junior teams, use &lt;strong&gt;ringall&lt;/strong&gt; with rank separating tiers.&lt;/p&gt;

&lt;p&gt;Open the inbound group configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/asterisk/extensions-vicidial.conf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 50 &lt;span class="s2"&gt;"exten =&amp;gt; 6001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the Queue application line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="m"&gt;6001&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Queue&lt;/span&gt;(&lt;span class="n"&gt;SALES&lt;/span&gt;|&lt;span class="n"&gt;t&lt;/span&gt;|${&lt;span class="n"&gt;DIALPLAN_VARS&lt;/span&gt;}|&lt;span class="m"&gt;5&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The queue name is SALES. The third parameter sets strategy. Modify the queues.conf file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 30 &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;SALES&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/asterisk/queues.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;SALES&lt;/span&gt;]
&lt;span class="n"&gt;music&lt;/span&gt; = &lt;span class="n"&gt;default&lt;/span&gt;
&lt;span class="n"&gt;strategy&lt;/span&gt; = &lt;span class="n"&gt;leastrecent&lt;/span&gt;
&lt;span class="n"&gt;timeout&lt;/span&gt; = &lt;span class="m"&gt;15&lt;/span&gt;
&lt;span class="n"&gt;retry&lt;/span&gt; = &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;weight&lt;/span&gt; = &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1001&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1002&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1003&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change to rrmemory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/strategy = leastrecent/strategy = rrmemory/'&lt;/span&gt; /etc/asterisk/queues.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload app_queue.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the change took effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Strategy: Round Robin with Memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Load balancing across inbound groups
&lt;/h3&gt;

&lt;p&gt;If you have multiple inbound groups (SALES, SUPPORT, BILLING), and agents belong to multiple groups, load balancing ensures each agent is not overloaded across all groups.&lt;/p&gt;

&lt;p&gt;In ViciDial, an agent can have a primary inbound_group and secondary groups. Rank applies to each group independently. However, if an agent is assigned to two groups, they will receive calls from both based on their rank in each.&lt;/p&gt;

&lt;p&gt;To prevent an agent from being overwhelmed, use the agent's max_concurrent_calls setting in vicidial_users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_concurrent_calls&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1001'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set max concurrent calls to 1 for single-call handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;max_concurrent_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1001'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enforced at the dial-plan level in extensions-vicidial.conf. Look for the agent's Local channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="m"&gt;1001&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/&lt;span class="m"&gt;1001&lt;/span&gt;@&lt;span class="n"&gt;default&lt;/span&gt;,,&lt;span class="n"&gt;TtHhkK&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second parameter (empty above) can be filled with options. Asterisk does not natively limit concurrent calls per extension; this is managed by ViciDial's state tracking in the vicidial_agent_log table.&lt;/p&gt;

&lt;p&gt;ViciDial tracks agent state (READY, INCALL, etc.) in memory and database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calls_today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_call_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_agent_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1001'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;campaign_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Asterisk dial-plan consults this table before ringing an agent. If the agent's status is not READY or RING, they are skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring inbound groups and rank assignment
&lt;/h2&gt;

&lt;p&gt;Log into the web admin console at &lt;code&gt;/vicidial/admin.php&lt;/code&gt;. Go to Inbound &amp;gt; Manage Inbound Groups.&lt;/p&gt;

&lt;p&gt;Create or edit a group (e.g., SALES):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Group ID: SALES&lt;/li&gt;
&lt;li&gt;Group Alias: Sales Inbound Queue&lt;/li&gt;
&lt;li&gt;Queue Seconds: 15 (ring timeout)&lt;/li&gt;
&lt;li&gt;Queue Priority: 1 (higher numbers = lower priority if multiple groups)&lt;/li&gt;
&lt;li&gt;Calls Today: 0 (auto)&lt;/li&gt;
&lt;li&gt;Allow Ingroup Recording: Yes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save. Then go to Manage Inbound Group Members. Add agents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent ID&lt;/th&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1002&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1003&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The web interface updates vicidial_users.user_rank for each agent. Verify via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT user_id, user_rank, inbound_group FROM vicidial_users WHERE inbound_group = 'SALES';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing call distribution
&lt;/h2&gt;

&lt;p&gt;Start with two agents logged in to the same inbound group.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"agent show 1001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output should show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent/1001 (Alice) is logged in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat for agent 1002:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"agent show 1002"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, send an inbound call to the group's DID. Use a test call or generate one via a curl script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8088/stasis/applications/vicidial_inbound/subscribe?channel=SIP/trunk%2F5551234567"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you have an existing inbound trunk, dial the number manually.&lt;/p&gt;

&lt;p&gt;Watch Asterisk console in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"(Queue|Agent|Member)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If agent 1001 is READY, they ring first. If they go INCALL, watch for agent 1002 to ring next. If rank is wrong (e.g., both at rank 1), both ring simultaneously (depending on strategy).&lt;/p&gt;

&lt;p&gt;Check the Asterisk queue status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SALES has 1 calls (max unlimited) in 'rrmemory' strategy (20s holdtime, 45s talktime)
  Members:
    Local/1001@vicidial (Member) Penalty: 0 Paused: No (In use)
    Local/1002@vicidial (Member) Penalty: 0 Paused: No (Ready)
    Local/1003@vicidial (Member) Penalty: 0 Paused: No (Ready)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Member status tells you who is ringing or in call. If it says (In use), that agent's status is INCALL in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: custom rank-based load balancing
&lt;/h2&gt;

&lt;p&gt;For high-volume or multi-team setups, use Asterisk's "penalty" system in queues.conf to override rank. Penalty increases the time an agent is skipped before being rung.&lt;/p&gt;

&lt;p&gt;Edit queues.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;SALES&lt;/span&gt;]
&lt;span class="n"&gt;strategy&lt;/span&gt; = &lt;span class="n"&gt;leastrecent&lt;/span&gt;
&lt;span class="n"&gt;timeout&lt;/span&gt; = &lt;span class="m"&gt;15&lt;/span&gt;
&lt;span class="n"&gt;queue&lt;/span&gt;-&lt;span class="n"&gt;yoursoundfile&lt;/span&gt; = &lt;span class="n"&gt;on&lt;/span&gt;

&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1001&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1002&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1003&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1004&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents 1003 and 1004 have higher penalties, so they are deprioritized even if they are READY. Agent 1003 is rung only after agents 1001 and 1002 have declined or are busy. Agent 1004 is a last resort.&lt;/p&gt;

&lt;p&gt;Reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload app_queue.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Members:
  Local/1001@vicidial (Member) Penalty: 0 Paused: No
  Local/1002@vicidial (Member) Penalty: 0 Paused: No
  Local/1003@vicidial (Member) Penalty: 10 Paused: No
  Local/1004@vicidial (Member) Penalty: 20 Paused: No
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Penalty works independently of rank. If you use both rank and penalty, Asterisk applies rank first (within vicidial_users), then penalty (in queues.conf) as a secondary filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and tuning rank distribution
&lt;/h2&gt;

&lt;p&gt;Log in to the ViciDial admin panel. Go to Reports &amp;gt; Agent Performance.&lt;/p&gt;

&lt;p&gt;Filter by Inbound Group: SALES. Check the Calls Handled column for each agent over the last hour.&lt;/p&gt;

&lt;p&gt;Expected distribution for equal-rank agents: roughly equal call counts. If one agent has 20 calls and another has 5, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent availability: Is the low-call agent paused or offline?&lt;/li&gt;
&lt;li&gt;Rank: Did you apply the correct rank to all agents?&lt;/li&gt;
&lt;li&gt;Queue strategy: Is it set to leastrecent or rrmemory?&lt;/li&gt;
&lt;li&gt;Agent skill: Do some agents handle calls longer, reducing their READY time?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example query to check agent call volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;call_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;inbound_group_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If agent 1001 has handled 15 calls and agent 1003 has handled 2, and both have equal rank, check agent 1003's inbound_group assignment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1003'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If inbound_group is NULL or different, they are not in the same queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does ViciDial skip agents even when they are available?
&lt;/h2&gt;

&lt;p&gt;Agents are skipped if their status in vicidial_agent_log is not READY. Check directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT user_id, status FROM vicidial_agent_log WHERE user_id IN ('1001','1002','1003');"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible statuses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;READY: Available for calls&lt;/li&gt;
&lt;li&gt;INCALL: On an active call&lt;/li&gt;
&lt;li&gt;PAUSED: Manually paused by agent or admin&lt;/li&gt;
&lt;li&gt;RING: Phone is ringing, awaiting answer&lt;/li&gt;
&lt;li&gt;DISPO: Entering call disposition&lt;/li&gt;
&lt;li&gt;DEAD: Disconnected or logged off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an agent shows DEAD or PAUSED when you expect READY, they were disconnected or paused. In the agent screen at /agc/vicidial.php, the agent can click Ready, or an admin can force the status in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_agent_log&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'READY'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1003'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;campaign_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the agent's Asterisk session to propagate the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"hangup request channel SIP/1003@*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent must log back into the agent screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling agent rank changes without dropping calls
&lt;/h2&gt;

&lt;p&gt;If you change rank while an agent is logged in, the change takes effect on the next call offered to that agent, not immediately. Existing INCALL agents are not affected.&lt;/p&gt;

&lt;p&gt;To change rank safely during business hours:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check current active calls:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;If few calls are active, update rank:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1003'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Reload the queue in Asterisk:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload app_queue.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Verify change took effect:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If calls drop, it means the extension or agent was in the queue at the moment of reload. This is rare but possible in high-traffic systems. During production, make rank changes at low-traffic windows (nights, weekends) or disable the inbound group temporarily:&lt;/p&gt;

&lt;p&gt;Go to Inbound &amp;gt; Manage Inbound Groups, set the group status to Inactive, save. Agents will go PAUSED. Update rank. Re-enable the group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting rank and load-balancing issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Calls queue instead of ringing agents
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Calls sit in queue, hold time increases, agents have zero calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 1&lt;/strong&gt;: Agents are offline or in wrong group.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT user_id, inbound_group, status FROM vicidial_agent_log WHERE campaign_id = 'SALES';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no rows return, no agents are logged into SALES. Ensure agents log into the correct group at /agc/vicidial.php.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 2&lt;/strong&gt;: Queue members are not properly registered.&lt;/p&gt;

&lt;p&gt;Check extensions-vicidial.conf for the queue context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 5 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;vicidial&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/asterisk/extensions-vicidial.conf | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;XXX&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;}@&lt;span class="n"&gt;default&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent extension is not properly registered in SIP, Dial fails silently. Check SIP peers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peers"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent 1001 should appear as SIP/1001 (if registered) or SIP/1001@default (if via trunk). If not listed, they are not registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 3&lt;/strong&gt;: Queue strategy does not match agent load.&lt;/p&gt;

&lt;p&gt;If using linear strategy, calls ring agents one at a time, waiting for timeout. With 20 agents and a 15-second timeout, the queue waits 15 seconds per agent before moving to the next. For the 5th agent, the caller waits 75 seconds.&lt;/p&gt;

&lt;p&gt;Change strategy to leastrecent or rrmemory in queues.conf.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calls always route to the same agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Agent 1001 handles all calls. Agents 1002, 1003 never ring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 1&lt;/strong&gt;: Rank is not set for other agents.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If user_rank is NULL for agents 1002 and 1003, they have no rank and are not in the queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Assign rank to all agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1002'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;user_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1003'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;inbound_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SALES'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload app_queue.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cause 2&lt;/strong&gt;: Agent 1001 is the only one in the queue.&lt;/p&gt;

&lt;p&gt;Check queues.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 10 &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;SALES&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/asterisk/queues.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If only one member is listed, manually add others or regenerate the file via the ViciDial admin. Go to Inbound &amp;gt; Manage Inbound Groups, select SALES, and confirm all agents are listed. Save and check queues.conf again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 3&lt;/strong&gt;: Other agents have a higher penalty or invalid penalty syntax.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If agents show Penalty: 999 or Penalty: infinity, they are effectively disabled. Edit queues.conf and set penalty to a numeric value (0-999):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1001&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1002&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;member&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;Local&lt;/span&gt;/&lt;span class="m"&gt;1003&lt;/span&gt;@&lt;span class="n"&gt;vicidial&lt;/span&gt;,&lt;span class="n"&gt;penalty&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Agent is rung twice for one call
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Agent 1001 gets two simultaneous rings for a single inbound call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Agent is listed twice in queues.conf or the member list was not reloaded.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"1001@vicidial"&lt;/span&gt; /etc/asterisk/queues.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the line appears twice, remove the duplicate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'0,/member =&amp;gt; Local\/1001@vicidial/!b; /member =&amp;gt; Local\/1001@vicidial/{x;d;}'&lt;/span&gt; /etc/asterisk/queues.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This removes the first occurrence; adjust if you need to remove a different one.)&lt;/p&gt;

&lt;p&gt;Reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload app_queue.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  One agent's calls have long hold times despite low call volume
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Agent 1002 gets calls with 5-minute hold times. Agent 1001 has zero hold time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 1&lt;/strong&gt;: Agent 1002 has a higher penalty and is being skipped.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"queue show SALES"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;1002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Penalty is high (&amp;gt;10), lower it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause 2&lt;/strong&gt;: Agent 1001's phone is ringing all calls but agent is not answering.&lt;/p&gt;

&lt;p&gt;Check the agent's extension registration and call logs. Is agent 1001 repeatedly rejecting calls or letting them time out?&lt;/p&gt;

&lt;p&gt;Look at recent calls for agent 1001:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1001'&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If duration is 0 and status is ABANDONED, the call was not answered. Check if agent 1001's phone is registered and online in SIP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peers"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;1001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If unreachable or offline, agent will not answer calls. Ask agent to log in via SIP client or check their phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my inbound group show zero members after I assign rank to agents?
&lt;/h3&gt;

&lt;p&gt;The queue members are populated from vicidial_users at module reload time. If rank is NULL or inbound_group does not match the queue name, the agent is not added. Check that inbound_group is set to the exact queue name (case-sensitive) and user_rank is a number between 0 and 9999. Then run &lt;code&gt;asterisk -rx "module reload app_queue.so"&lt;/code&gt; and verify with &lt;code&gt;queue show [groupname]&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I assign the same rank to multiple agents?
&lt;/h3&gt;

&lt;p&gt;Yes, but behavior depends on strategy. With ringall, both ring simultaneously. With leastrecent or rrmemory, the system tracks which agent was called least recently or last and picks that one. Assigning the same rank to two agents is equivalent to having no rank separation; use it only if agents have identical skill levels and you want simultaneous ring.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prioritize VIP callers to a specific agent or rank?
&lt;/h3&gt;

&lt;p&gt;ViciDial does not have built-in VIP queue routing by caller ID, but you can create a separate inbound group for VIP calls with a single dedicated agent, then route DID entries to that group. Alternatively, use a dialplan variable to set a higher priority in the queue by checking inbound_cid against a list and using asterisk's priorities or a custom AGI script to inject the call higher in the queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I delete or merge two inbound groups?
&lt;/h3&gt;

&lt;p&gt;If you delete SALES inbound group in the web admin, all agents in SALES lose their inbound_group assignment (it becomes NULL), and calls to DIDs mapped to SALES will fail because the queue no longer exists. To merge SALES and SALES_BACKUP, update all agents in SALES_BACKUP to inbound_group = 'SALES' and delete SALES_BACKUP. Update the DID routing if necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do rank and penalty interact in Asterisk?
&lt;/h3&gt;

&lt;p&gt;Rank is applied first by ViciDial's dial-plan; it determines which agents are in the queue and in which order they are listed. Penalty is then applied by Asterisk's queue engine to further reorder or deprioritize agents. If rank 1 has 0 penalty and rank 2 has 20 penalty, rank 1 is always called first; rank 2 is called only if rank 1 is not available. Penalty does not override rank; it supplements it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Agent rank in ViciDial controls the order in which agents are offered calls within an inbound group. Setting rank correctly requires updating the vicidial_users table (user_rank field), choosing an appropriate queue strategy (leastrecent or rrmemory for most setups), and verifying agent inbound_group assignment matches the queue name. Load balancing is further refined using Asterisk's penalty system in queues.conf to deprioritize overflow agents or skill-based tiers. Testing is done by logging in two or more agents, sending test calls, and monitoring queue and agent status via Asterisk CLI. Common issues include NULL rank, incorrect inbound_group, offline agents, duplicate queue members, and mismatched strategies. Monitor call distribution via the agent performance report or direct SQL queries on vicidial_log to verify even load. Changes to rank take effect on the next call offered after a queue reload; high-traffic systems should make rank changes during off-peak hours or disable the group temporarily.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>Query Millions of VoIP CDRs with DuckDB on Budget VPS</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Tue, 20 Oct 2026 03:30:03 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/query-millions-of-voip-cdrs-with-duckdb-on-budget-vps-422g</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/query-millions-of-voip-cdrs-with-duckdb-on-budget-vps-422g</guid>
      <description>&lt;h1&gt;
  
  
  Query Millions of VoIP CDRs with DuckDB on Budget VPS
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Extract and analyze call records from Asterisk and ViciDial in seconds on 1GB RAM by switching from SQL to column-oriented format.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DuckDB queries raw CDR data 10 to 100 times faster than MySQL because it operates on compressed columnar storage instead of row-by-row lookups. On a budget VPS with 1GB RAM, you can load millions of call records from ViciDial's vicidial_log table, run complex aggregations on agent performance, call duration, and carrier routes, then export results in seconds. The setup requires only a single binary, basic file format conversion, and a cron job. This tutorial walks through importing real Asterisk CDRs into DuckDB, writing practical queries for call center operations, and troubleshooting the gaps between what ViciDial logs and what you actually need to see.&lt;/p&gt;

&lt;p&gt;Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, DuckDB 0.9.1+, on Debian 11/12 with 1GB RAM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Running ViciDial instance with MariaDB or MySQL backend&lt;/li&gt;
&lt;li&gt;SSH access to the VPS where ViciDial runs (or a separate analytics host)&lt;/li&gt;
&lt;li&gt;Asterisk 16+ with CDR logging enabled&lt;/li&gt;
&lt;li&gt;500 MB free disk space for sample data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wget&lt;/code&gt; or &lt;code&gt;curl&lt;/code&gt; to download DuckDB binary&lt;/li&gt;
&lt;li&gt;Basic knowledge of SQL SELECT statements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No GUI tools needed. All work happens in the shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why switch from MySQL to DuckDB for call records?
&lt;/h2&gt;

&lt;p&gt;When you have 10 million call records in vicidial_log, a MySQL query like "SELECT COUNT(*), AVG(LENGTH_IN_SEC) FROM vicidial_log WHERE date BETWEEN..." can take 30 seconds or more. The database must scan every row, decompress fields, and filter. DuckDB pre-compresses data by column: all the LENGTH_IN_SEC values live together, indexed and packed, so the same query finishes in under a second on a single-core VPS.&lt;/p&gt;

&lt;p&gt;The tradeoff: DuckDB is read-only for analytics. You do not replace ViciDial's live MariaDB. Instead, you export CDR snapshots (daily or hourly) into Parquet or CSV format, load them into DuckDB, and run reports offline. Write operations stay in MySQL. This pattern works because call records rarely change after the call ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Download and install DuckDB
&lt;/h2&gt;

&lt;p&gt;SSH into your VPS and install the DuckDB CLI binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt
wget https://github.com/duckdb/duckdb/releases/download/v0.9.1/duckdb_cli-linux-x86_64.zip
unzip duckdb_cli-linux-x86_64.zip
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x duckdb
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /opt/duckdb /usr/local/bin/duckdb
duckdb &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb :memory: &lt;span class="s2"&gt;"SELECT 1 as test;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────┐
│ test   │
│ int64  │
├────────┤
│ 1      │
└────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are on a system with restricted &lt;code&gt;/opt&lt;/code&gt; permissions, extract to &lt;code&gt;/home/vicidial/&lt;/code&gt; or &lt;code&gt;/tmp/&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Export ViciDial CDRs to CSV format
&lt;/h2&gt;

&lt;p&gt;ViciDial stores call logs in the &lt;code&gt;vicidial_log&lt;/code&gt; table. You will export a date range to a CSV file that DuckDB can ingest. Use the MariaDB client directly on the VPS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT 
  call_date, 
  call_time, 
  caller_id_number, 
  called_party_number, 
  length_in_sec, 
  agent_id, 
  campaign_id, 
  call_status, 
  lead_id, 
  phone_number,
  user_id
FROM vicidial_log 
WHERE call_date &amp;gt;= '2024-01-15' AND call_date &amp;lt;= '2024-01-21'
INTO OUTFILE '/tmp/vicidial_cdr.csv' 
FIELDS TERMINATED BY ',' 
ENCLOSED BY '&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;' 
LINES TERMINATED BY '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are on a managed host and do not have OUTFILE permission, use an alternative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;--batch&lt;/span&gt; &lt;span class="nt"&gt;--skip-column-names&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT call_date, call_time, caller_id_number, called_party_number, &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  length_in_sec, agent_id, campaign_id, call_status, lead_id, phone_number, user_id &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  FROM vicidial_log WHERE call_date &amp;gt;= '2024-01-15'"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\t/,/g'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/vicidial_cdr.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the file was created and contains data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /tmp/vicidial_cdr.csv
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; /tmp/vicidial_cdr.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="mf"&gt;1247563&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;tmp&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;vicidial&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;cdr&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;csv&lt;/span&gt;
&lt;span class="ld"&gt;2024-01-15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;08:23:45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5551234567&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5559876543&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SALES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;LIVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;55412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5559876543&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1023&lt;/span&gt;
&lt;span class="ld"&gt;2024-01-15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;08:45:12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5551234560&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5559876544&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;156&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SALES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;LIVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;55413&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5559876544&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Create a DuckDB database and import the CSV
&lt;/h2&gt;

&lt;p&gt;Initialize a DuckDB file and create a table from the CSV.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
CREATE TABLE vicidial_log AS
SELECT 
  CAST(call_date AS DATE) as call_date,
  call_time,
  caller_id_number,
  called_party_number,
  CAST(length_in_sec AS INTEGER) as length_in_sec,
  CAST(agent_id AS VARCHAR) as agent_id,
  campaign_id,
  call_status,
  CAST(lead_id AS BIGINT) as lead_id,
  phone_number,
  CAST(user_id AS INTEGER) as user_id
FROM read_csv_auto('/tmp/vicidial_cdr.csv', sample_size=-1);

SELECT COUNT(*) as record_count, 
  MIN(call_date) as earliest_date,
  MAX(call_date) as latest_date
FROM vicidial_log;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB will auto-detect column types. Verify the import by querying row and date counts.&lt;/p&gt;

&lt;p&gt;Expected output (adjust numbers to your data):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┬───────────────┬─────────────┐
│ record_count │ earliest_date │ latest_date │
│   1247562    │   2024-01-15  │  2024-01-21 │
└──────────────┴───────────────┴─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Automate daily CDR exports and DuckDB ingestion
&lt;/h2&gt;

&lt;p&gt;Create a shell script that runs via cron each night to pull the previous day's records and append them to the DuckDB database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /home/vicidial/sync_cdr_to_duckdb.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SCRIPT&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash

VICIDIAL_HOME="/home/vicidial"
DB_FILE="&lt;/span&gt;&lt;span class="nv"&gt;$VICIDIAL_HOME&lt;/span&gt;&lt;span class="sh"&gt;/cdr_analysis.duckdb"
CSV_TMP="/tmp/vicidial_cdr_daily.csv"
YESTERDAY=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"yesterday"&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
YESTERDAY_END=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"yesterday"&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
LOG_FILE="/var/log/duckdb_sync.log"

echo "[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;] Starting CDR sync for &lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="sh"&gt;

# Export yesterday's records from MariaDB
mysql -u root -p'password_here' asterisk --batch --skip-column-names -e &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  "SELECT call_date, call_time, caller_id_number, called_party_number, &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
   length_in_sec, agent_id, campaign_id, call_status, lead_id, phone_number, user_id &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
   FROM vicidial_log WHERE call_date = '&lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="sh"&gt;'" | &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  sed 's/&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;/,/g' &amp;gt; "&lt;/span&gt;&lt;span class="nv"&gt;$CSV_TMP&lt;/span&gt;&lt;span class="sh"&gt;"

if [ ! -s "&lt;/span&gt;&lt;span class="nv"&gt;$CSV_TMP&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  echo "[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;] No data found for &lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="sh"&gt;, exiting" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="sh"&gt;
  exit 0
fi

# Append to DuckDB
duckdb "&lt;/span&gt;&lt;span class="nv"&gt;$DB_FILE&lt;/span&gt;&lt;span class="sh"&gt;" &amp;lt;&amp;lt; EOF
INSERT INTO vicidial_log
SELECT 
  CAST(call_date AS DATE) as call_date,
  call_time,
  caller_id_number,
  called_party_number,
  CAST(length_in_sec AS INTEGER) as length_in_sec,
  CAST(agent_id AS VARCHAR) as agent_id,
  campaign_id,
  call_status,
  CAST(lead_id AS BIGINT) as lead_id,
  phone_number,
  CAST(user_id AS INTEGER) as user_id
FROM read_csv_auto('&lt;/span&gt;&lt;span class="nv"&gt;$CSV_TMP&lt;/span&gt;&lt;span class="sh"&gt;', sample_size=-1);

SELECT COUNT(*) as rows_inserted FROM (
  SELECT * FROM vicidial_log WHERE call_date = CAST('&lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="sh"&gt;' AS DATE)
);
EOF

RESULT=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="sh"&gt;
if [ &lt;/span&gt;&lt;span class="nv"&gt;$RESULT&lt;/span&gt;&lt;span class="sh"&gt; -eq 0 ]; then
  echo "[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;] Sync completed successfully" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="sh"&gt;
  rm -f "&lt;/span&gt;&lt;span class="nv"&gt;$CSV_TMP&lt;/span&gt;&lt;span class="sh"&gt;"
else
  echo "[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;] Sync failed with code &lt;/span&gt;&lt;span class="nv"&gt;$RESULT&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="sh"&gt;
fi
&lt;/span&gt;&lt;span class="no"&gt;SCRIPT

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /home/vicidial/sync_cdr_to_duckdb.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the script to use your actual MariaDB password, then add to crontab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 1 * * * /home/vicidial/sync_cdr_to_duckdb.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the sync script every day at 01:00 UTC (adjust the hour as needed).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Query call data by agent performance
&lt;/h2&gt;

&lt;p&gt;Now that CDRs are in DuckDB, write queries for common reporting needs. All examples assume your DuckDB file is at &lt;code&gt;/home/vicidial/cdr_analysis.duckdb&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Daily call volume and average handling time by agent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT 
  call_date,
  agent_id,
  COUNT(*) as calls_handled,
  ROUND(AVG(length_in_sec), 2) as avg_duration_sec,
  SUM(length_in_sec) as total_handle_time_sec,
  COUNT(CASE WHEN call_status = 'LIVE' THEN 1 END) as connected_calls,
  ROUND(100.0 * COUNT(CASE WHEN call_status = 'LIVE' THEN 1 END) / COUNT(*), 2) as connect_rate_pct
FROM vicidial_log
WHERE call_date &amp;gt;= '2024-01-15'
  AND call_date &amp;lt; '2024-01-22'
  AND agent_id IS NOT NULL
GROUP BY call_date, agent_id
ORDER BY call_date DESC, calls_handled DESC;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────┬──────────┬────────────────┬─────────────────────┬──────────────────────┬─────────────────┬──────────────────┐
│ call_date  │ agent_id │ calls_handled  │ avg_duration_sec    │ total_handle_time_sec│ connected_calls  │ connect_rate_pct │
│ 2024-01-21 │ 1023     │ 145            │ 187.43              │ 27176                │ 121              │ 83.45            │
│ 2024-01-21 │ 1024     │ 138            │ 201.12              │ 27754                │ 118              │ 85.51            │
│ 2024-01-20 │ 1023     │ 142            │ 183.22              │ 25996                │ 119              │ 83.80            │
└────────────┴──────────┴────────────────┴─────────────────────┴──────────────────────┴─────────────────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Campaign performance: total cost per lead by carrier
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT 
  campaign_id,
  call_status,
  COUNT(*) as attempt_count,
  COUNT(DISTINCT lead_id) as unique_leads,
  ROUND(AVG(length_in_sec), 1) as avg_duration_sec,
  SUM(CASE WHEN length_in_sec &amp;gt;= 10 THEN 1 ELSE 0 END) as calls_over_10sec
FROM vicidial_log
WHERE call_date &amp;gt;= '2024-01-15'
GROUP BY campaign_id, call_status
ORDER BY campaign_id, attempt_count DESC;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find long calls that may indicate transfer or hold
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT 
  call_date,
  call_time,
  agent_id,
  caller_id_number,
  called_party_number,
  length_in_sec,
  call_status
FROM vicidial_log
WHERE length_in_sec &amp;gt; 600
  AND call_date = '2024-01-21'
ORDER BY length_in_sec DESC
LIMIT 20;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hourly call volume distribution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT 
  call_date,
  SUBSTR(call_time, 1, 2) as hour,
  COUNT(*) as call_count,
  ROUND(AVG(length_in_sec), 1) as avg_duration_sec
FROM vicidial_log
WHERE call_date &amp;gt;= '2024-01-15'
GROUP BY call_date, SUBSTR(call_time, 1, 2)
ORDER BY call_date, hour;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Export query results for reporting
&lt;/h2&gt;

&lt;p&gt;DuckDB can output results in JSON, CSV, or Parquet formats. Use this to feed dashboards or spreadsheets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="nt"&gt;-json&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; /tmp/agent_report.json
SELECT 
  call_date,
  agent_id,
  COUNT(*) as calls_handled,
  ROUND(AVG(length_in_sec), 2) as avg_duration_sec
FROM vicidial_log
WHERE call_date &amp;gt;= '2024-01-15'
GROUP BY call_date, agent_id;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or export to CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
COPY (
  SELECT 
    call_date,
    agent_id,
    COUNT(*) as calls_handled,
    ROUND(AVG(length_in_sec), 2) as avg_duration_sec
  FROM vicidial_log
  WHERE call_date &amp;gt;= '2024-01-15'
  GROUP BY call_date, agent_id
) TO '/tmp/agent_report.csv' (FORMAT CSV, HEADER TRUE);
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retrieve the file for import into your BI tool or Excel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Handle missing or malformed CDR data
&lt;/h2&gt;

&lt;p&gt;Not all ViciDial configurations log every field consistently. You may encounter NULLs, empty strings, or misaligned columns.&lt;/p&gt;

&lt;p&gt;When importing, check for data quality first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT 
  COUNT(*) as total_rows,
  COUNT(agent_id) as rows_with_agent,
  COUNT(call_date) as rows_with_date,
  MIN(length_in_sec) as min_duration,
  MAX(length_in_sec) as max_duration
FROM vicidial_log;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a large number of NULL agent_ids, your ViciDial instance may not be logging agent interactions. Check that vicidial_log is configured to capture agent_id. In the ViciDial admin web interface, navigate to System Settings, then verify "Log Agent" is enabled.&lt;/p&gt;

&lt;p&gt;If you see negative or extremely large values in length_in_sec, add a filter to the import query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
CREATE TABLE vicidial_log_clean AS
SELECT * FROM vicidial_log
WHERE length_in_sec &amp;gt;= 0 AND length_in_sec &amp;lt;= 36000;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes calls with duration over 10 hours (likely corrupted records).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Set up a read-only DuckDB endpoint for other team members
&lt;/h2&gt;

&lt;p&gt;If you have multiple analysts on the VPS, you can create a simple shell wrapper that allows running queries without exposing the database file directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/local/bin/query_cdr &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SCRIPT&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
# Simple read-only CDR query wrapper

if [ -z "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  echo "Usage: query_cdr 'SELECT ...' [format]"
  echo "Format: table, json, csv (default: table)"
  exit 1
fi

FORMAT=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;table&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;

case &lt;/span&gt;&lt;span class="nv"&gt;$FORMAT&lt;/span&gt;&lt;span class="sh"&gt; in
  json)
    duckdb /home/vicidial/cdr_analysis.duckdb -json "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"
    ;;
  csv)
    duckdb /home/vicidial/cdr_analysis.duckdb &amp;lt;&amp;lt; EOF
COPY (&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;) TO '/dev/stdout' (FORMAT CSV, HEADER TRUE);
EOF
    ;;
  *)
    duckdb /home/vicidial/cdr_analysis.duckdb "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"
    ;;
esac
&lt;/span&gt;&lt;span class="no"&gt;SCRIPT

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/query_cdr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Team members can now run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;query_cdr &lt;span class="s2"&gt;"SELECT COUNT(*) FROM vicidial_log WHERE call_date = '2024-01-21'"&lt;/span&gt; csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: This does not implement row-level access control. For stricter security, use a web API wrapper such as DuckDB's official Python HTTP server, which can enforce authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Optimize DuckDB for very large datasets
&lt;/h2&gt;

&lt;p&gt;When your DuckDB file grows beyond 5GB, consider these optimizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Parquet format instead of CSV for imports
&lt;/h3&gt;

&lt;p&gt;Parquet is compressed and column-oriented, much faster to load than CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Export from MariaDB to Parquet via Python&lt;/span&gt;
python3 &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PYTHON&lt;/span&gt;&lt;span class="sh"&gt;'
import duckdb
import pandas as pd
import pymysql

conn = pymysql.connect(
  host="localhost",
  user="root",
  password="password",
  database="asterisk"
)

df = pd.read_sql(
  "SELECT * FROM vicidial_log WHERE call_date = '2024-01-21'",
  conn
)

duckdb_conn = duckdb.connect('/home/vicidial/cdr_analysis.duckdb')
duckdb_conn.register('df', df)
duckdb_conn.execute("""
  INSERT INTO vicidial_log SELECT * FROM df
""")
duckdb_conn.close()
conn.close()
&lt;/span&gt;&lt;span class="no"&gt;PYTHON
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Partition the database by date
&lt;/h3&gt;

&lt;p&gt;Instead of one large table, create separate tables or Parquet files by month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;month &lt;span class="k"&gt;in &lt;/span&gt;2024-01 2024-02 2024-03&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM vicidial_log WHERE call_date LIKE '&lt;/span&gt;&lt;span class="nv"&gt;$month&lt;/span&gt;&lt;span class="s2"&gt;%' INTO OUTFILE '/tmp/cdr_&lt;/span&gt;&lt;span class="nv"&gt;$month&lt;/span&gt;&lt;span class="s2"&gt;.csv' ..."&lt;/span&gt; &amp;amp;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then load each month into a separate DuckDB instance or use DuckDB's partitioned table support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DuckDB returns "No such table" error
&lt;/h3&gt;

&lt;p&gt;Verify the table exists by connecting to the database file and listing tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="s2"&gt;".tables"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the table is missing, reimport the CSV. Confirm that the CSV file path in the read_csv_auto() function is absolute or relative to the working directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cron job for sync_cdr_to_duckdb.sh did not run
&lt;/h3&gt;

&lt;p&gt;Check the cron log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"sync_cdr_to_duckdb"&lt;/span&gt; /var/log/syslog
&lt;span class="c"&gt;# or on systemd-based systems:&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the script has execute permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /home/vicidial/sync_cdr_to_duckdb.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the script manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/home/vicidial/sync_cdr_to_duckdb.sh
&lt;span class="nb"&gt;cat&lt;/span&gt; /var/log/duckdb_sync.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CSV import is very slow on large files
&lt;/h3&gt;

&lt;p&gt;For files over 100MB, increase DuckDB's thread count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SET threads = 4;
CREATE TABLE vicidial_log AS SELECT * FROM read_csv_auto('/tmp/vicidial_cdr.csv');
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pre-compress the CSV using gzip and import directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;gzip&lt;/span&gt; /tmp/vicidial_cdr.csv
duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
CREATE TABLE vicidial_log AS SELECT * FROM read_csv_auto('/tmp/vicidial_cdr.csv.gz');
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DuckDB process runs out of memory on 1GB VPS
&lt;/h3&gt;

&lt;p&gt;DuckDB is designed for in-memory performance, but on constrained hardware, you may hit swap. Reduce the number of concurrent queries or partition your data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Limit DuckDB to 512MB memory&lt;/span&gt;
duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SET memory_limit = '512MB';
SELECT * FROM vicidial_log WHERE call_date = '2024-01-21' LIMIT 100;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or reduce the date range per query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Query by single day instead of full week&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;day &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;15..21&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
SELECT * FROM vicidial_log WHERE call_date = '2024-01-&lt;/span&gt;&lt;span class="nv"&gt;$day&lt;/span&gt;&lt;span class="sh"&gt;' ...
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Query results include rows from calls without an agent_id
&lt;/h3&gt;

&lt;p&gt;By design, DuckDB reads all rows. If your reporting requirement is "only calls handled by agents," filter explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;SELECT &lt;span class="k"&gt;*&lt;/span&gt; FROM vicidial_log
WHERE call_date &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-21'&lt;/span&gt;
  AND agent_id IS NOT NULL
  AND agent_id &amp;lt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or create a materialized view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
CREATE VIEW agent_calls AS
SELECT * FROM vicidial_log
WHERE agent_id IS NOT NULL AND agent_id &amp;lt;&amp;gt; '';
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query the view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;SELECT COUNT&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; FROM agent_calls WHERE call_date &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-21'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does DuckDB require me to export CDRs first instead of querying MariaDB directly?
&lt;/h3&gt;

&lt;p&gt;DuckDB is a columnar analytical database optimized for aggregations on static snapshots. MariaDB is a transactional row store optimized for updates. If you query MariaDB directly via a tool, it will still scan full tables row-by-row. DuckDB's advantage comes from copying the data into its compressed columnar format, which trades write speed for read speed. For live agent dashboards, keep querying MariaDB. For historical reports and bulk analysis, use DuckDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I delete old records from DuckDB to save disk space?
&lt;/h3&gt;

&lt;p&gt;Yes. DuckDB tables support DELETE statements. To remove records older than 90 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb /home/vicidial/cdr_analysis.duckdb &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
DELETE FROM vicidial_log WHERE call_date &amp;lt; DATE_SUB(CURRENT_DATE, INTERVAL 90 DAY);
VACUUM;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VACUUM command reclaims disk space. Alternatively, do not import data older than your retention window in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I query DuckDB from a Python script without shelling out to the CLI?
&lt;/h3&gt;

&lt;p&gt;Install the DuckDB Python package and use it as a library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your Python code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/home/vicidial/cdr_analysis.duckdb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM vicidial_log WHERE call_date = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2024-01-21&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids spawning a subprocess and is faster for multiple queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my ViciDial instance uses a different table schema, with custom columns?
&lt;/h3&gt;

&lt;p&gt;Export only the columns that exist and are relevant to your analysis. Check your schema first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"DESC vicidial_log;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adapt the SELECT statement in the export script to include only those columns. DuckDB will infer types automatically. If a column is missing, add it to the export and then add it to the CREATE TABLE statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can DuckDB work with live Asterisk CEL (Channel Event Log) data instead of ViciDial's vicidial_log?
&lt;/h3&gt;

&lt;p&gt;Yes. Asterisk's CEL table in the asterisk database contains finer-grained events. Export it the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM cel WHERE eventtime &amp;gt; DATE_SUB(NOW(), INTERVAL 1 DAY) INTO OUTFILE '/tmp/asterisk_cel.csv' ..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import to DuckDB. CEL data is more voluminous than CDR summaries, so expect larger files and longer query times. Filter by eventtype if you only need call setup, connect, and hangup events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You have set up a production CDR analytics pipeline using DuckDB on a budget VPS. The workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export daily call records from ViciDial's vicidial_log table to CSV format via MySQL.&lt;/li&gt;
&lt;li&gt;Load the CSV into a DuckDB database file using read_csv_auto() and INSERT statements.&lt;/li&gt;
&lt;li&gt;Run analytical queries on millions of records in milliseconds, grouping by agent, campaign, status, and time.&lt;/li&gt;
&lt;li&gt;Export results to JSON or CSV for reporting.&lt;/li&gt;
&lt;li&gt;Automate the daily import via a cron job.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next steps: adjust the sync script to match your MariaDB credentials and backup schedule, add Parquet file exports for archival beyond 90 days, and consider setting up a scheduled report that emails query results to your management team each morning. Monitor the DuckDB file size and cron logs weekly to catch any import failures early. If queries slow down as data accumulates, partition by month or switch to quarterly analysis windows instead of appending to a single table indefinitely.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>AI-Assisted VoIP Call Center Ops: Claude Hooks &amp; Incident KB</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Mon, 19 Oct 2026 03:30:03 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/ai-assisted-voip-call-center-ops-claude-hooks-incident-kb-550</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/ai-assisted-voip-call-center-ops-claude-hooks-incident-kb-550</guid>
      <description>&lt;h1&gt;
  
  
  AI-Assisted VoIP Call Center Ops: Claude Hooks &amp;amp; Incident KB
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Route inbound calls to the right agent queue in real time, auto-resolve carrier codec errors, and reduce mean time to recovery (MTTR) by 60% with Claude-powered incident detection in ViciDial.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Silence, one-way audio, and dropped calls in ViciDial usually trace to three causes: SIP codec mismatch between carrier and agent trunk, RTP socket timeout from firewall rule drift, or call state corruption in the vicidial_log table when agents go unavailable mid-call. Claude hooks hooked into your Asterisk dialplan can detect these patterns in near real time, query the ViciDial incident knowledge base, and auto-remediate by forcing G.711 transcoding, flushing stale RTP sessions, or rolling back agent call state without human intervention.&lt;/p&gt;

&lt;p&gt;This guide covers building a production Claude integration for VoIP call center operations, including incident detection hooks in the Asterisk dialplan, a queryable incident knowledge base in ViciDial, and practical troubleshooting workflows for the most common failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, and Claude API (claude-3-sonnet-20250229 or later).&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ViciDial 2.14 or later with Asterisk 16/18 running on CentOS 7 or Ubuntu 20.04 LTS.&lt;/li&gt;
&lt;li&gt;Direct SSH access to the ViciDial server and permission to modify /etc/asterisk/ files.&lt;/li&gt;
&lt;li&gt;MariaDB 10.5+ with root access to the asterisk database.&lt;/li&gt;
&lt;li&gt;Python 3.10+ installed on the ViciDial box (or a separate worker box with network access to Asterisk and MySQL).&lt;/li&gt;
&lt;li&gt;Claude API key with allowance for ~100-200 tokens per incident (cost ~$0.001 per incident).&lt;/li&gt;
&lt;li&gt;Familiarity with Asterisk dialplan syntax (extensions-vicidial.conf, sip-vicidial.conf).&lt;/li&gt;
&lt;li&gt;A text editor (nano, vim) and basic shell scripting.&lt;/li&gt;
&lt;li&gt;curl or a Python HTTP library to post logs to Claude.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why AI for call center ops at all?
&lt;/h2&gt;

&lt;p&gt;Call centers generate logs faster than humans read them. A single SIP carrier misconfiguration can spawn 500 error lines in Asterisk console within seconds. The pattern is obvious to Claude: you see two consecutive CODEC MISMATCH warnings followed by RTCP TIMEOUT, and you know the inbound trunk is configured for uLaw but the agent endpoint reports aLaw. Claude can spot this correlation, query your carrier contract database, auto-suggest the fix, or execute it directly via AGI. This saves your on-call engineer 20 minutes of grep and packet sniffing per incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;The workflow is three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dialplan hooks&lt;/strong&gt; in extensions-vicidial.conf that invoke a Python AGI script on call events (answer, hangup, transfer).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident detection logic&lt;/strong&gt; written as a Python AGI that logs call state to MySQL and calls Claude with structured prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude response&lt;/strong&gt; parsed into remediation actions: restart a trunk, force codec, update call state, or page an oncall engineer via webhook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The incident knowledge base lives in two tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vicidial_incident_log: raw call state at the moment of failure.&lt;/li&gt;
&lt;li&gt;vicidial_incident_kb: a curated collection of known issues, root causes, and fixes that you feed to Claude as context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up the incident knowledge base tables
&lt;/h2&gt;

&lt;p&gt;First, create the knowledge base tables in the asterisk database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;asterisk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vicidial_incident_kb&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;incident_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;incident_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;symptom&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;root_cause&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;detection_method&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;fix_command&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fix_priority&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_date&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_date&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="nb"&gt;TINYINT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vicidial_incident_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;call_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;agent_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;carrier_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;trunk_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_timestamp&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_detail&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;codec_in&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;codec_out&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;rtp_packets_lost&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;one_way_audio&lt;/span&gt; &lt;span class="nb"&gt;TINYINT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jitter_ms&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolved_by&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;resolution_time&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;claude_response&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_call_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_carrier&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_trunk&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trunk_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_event_type&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_timestamp&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;vicidial_incident_kb&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symptom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root_cause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detection_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fix_command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fix_priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'codec_mismatch_ulaw_alaw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'One-way audio or silence after answer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Inbound SIP carrier set to uLaw but agent endpoint is aLaw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CODEC MISMATCH in console AND RTCP timeout within 3 seconds'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'asterisk -rx "sip set debug on"; then review sip_vicidial.conf disallow/allow'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rtp_timeout_firewall'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Audio drops after 30-45 seconds mid-call'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Firewall rule for RTP socket timeout or NAT traversal failure'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'RTP socket timeout in Asterisk logs; traceroute shows packet loss on port range 10000-20000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Check iptables -L -n; verify udp 10000:20000 is ACCEPT for carrier IP; restart SIP channel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stale_call_state_db'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Agent marked unavailable but still in call queue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'vicidial_log entry not updated when call ended; race condition in hangup handler'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SELECT status FROM vicidial_log WHERE uniqueid matches call; status is still QUEUE or IN-CALL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UPDATE vicidial_log SET status=DONE WHERE uniqueid=X; restart vicidial agent screen'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dtmf_timeout_ivr'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IVR does not register DTMF keypresses'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SIP INFO frame setting incorrect; uLaw audio frame timing drift'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'No DTMF events in Asterisk verbose logs; SIP debug shows no INFO frames'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'asterisk -rx "sip set debug on"; verify dtmf_mode=auto in sip_vicidial.conf; check carrier SIP headers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'carrier_registration_failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Inbound trunk shows UNREACHABLE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SIP REGISTER to carrier fails: bad auth or IP blacklist'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Asterisk shows "Registration failed for X" on console; carrier portal shows failed logins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Verify SIP credentials; check IP whitelist at carrier; asterisk -rx "sip show registry"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now insert some baseline context for Claude. This table will grow over time as you encounter and document new issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the AGI hook script
&lt;/h2&gt;

&lt;p&gt;Create a Python AGI script at /usr/local/bin/vicidial_incident_agi.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
ViciDial incident detection AGI hook.
Logs call state events to vicidial_incident_log and calls Claude for analysis.
Usage: Add to extensions-vicidial.conf under appropriate contexts.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration
&lt;/span&gt;&lt;span class="n"&gt;MYSQL_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MYSQL_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MYSQL_PASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_db_password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MYSQL_DB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asterisk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;CLAUDE_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLAUDE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CLAUDE_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-sonnet-20250229&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;ASTERISK_LOG_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/log/asterisk/messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VicidalIncidentAGI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MYSQL_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MYSQL_PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MYSQL_DB&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_character_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf8mb4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB connection failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_agi_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read AGI variables from Asterisk.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agi_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write to Asterisk console and syslog.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VERBOSE &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt; 3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_kb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symptom_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch matching KB entries for Claude context.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictCursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;placeholders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symptom_keywords&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT incident_name, symptom, root_cause, fix_command, fix_priority
                FROM vicidial_incident_kb
                WHERE enabled = 1 AND (
                    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; OR &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symptom LIKE %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;symptom_keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
                    OR
                    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; OR &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident_name LIKE %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;symptom_keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
                )
                ORDER BY fix_priority DESC LIMIT 10
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;symptom_keywords&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KB query failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tail_asterisk_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read last N lines of Asterisk log for context.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASTERISK_LOG_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;log_lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Log read failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_incident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write event to vicidial_incident_log.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;agent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agi_extension&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;carrier_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agi_arg_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;trunk_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agi_arg_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO vicidial_incident_log
                (call_id, agent_id, carrier_id, trunk_name, event_type, event_detail, event_timestamp)
                VALUES (%s, %s, %s, %s, %s, %s, NOW())
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trunk_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incident log failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Call Claude API with incident context.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLAUDE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLAUDE_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.anthropic.com/v1/messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude API call failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_call_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Main event handler.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_agi_input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;call_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agi_uniqueid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agi_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agi_arg_0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incident AGI: call=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, event=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Determine if this is a problem event
&lt;/span&gt;        &lt;span class="n"&gt;problem_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CODEC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MISMATCH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIMEOUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNREACHABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;asterisk_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tail_asterisk_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;has_problem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;asterisk_log&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;problem_keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_problem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Query KB for similar issues
&lt;/span&gt;        &lt;span class="n"&gt;kb_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_kb&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unreachable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;kb_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;incident_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;symptom&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (fix: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fix_command&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kb_results&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="c1"&gt;# Build prompt for Claude
&lt;/span&gt;        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are a VoIP SIP call center operations expert. A call center incident was detected:

Call ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Event type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Recent Asterisk logs (last 100 lines):
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;asterisk_log&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Known similar incidents from our KB:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kb_context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Based on the logs and KB, identify:
1. Most likely root cause (one sentence).
2. Recommended immediate action (one sentence).
3. If safe to auto-remediate, suggest the exact CLI command.
4. Otherwise, suggest what to check first (one sentence).

Keep responses concise. Assume the operator has SSH access to the Asterisk box.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_claude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude analysis: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_incident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_incident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude call failed; check logs.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VicidalIncidentAGI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;agi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_call_event&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;agi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the script executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/vicidial_incident_agi.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hooking the AGI into the dialplan
&lt;/h2&gt;

&lt;p&gt;Edit /etc/asterisk/extensions-vicidial.conf and add a macro or context that invokes the AGI on inbound calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;macro&lt;/span&gt;-&lt;span class="n"&gt;incident&lt;/span&gt;-&lt;span class="n"&gt;hook&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;s&lt;/span&gt;,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Incident&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="n"&gt;for&lt;/span&gt; ${&lt;span class="n"&gt;UNIQUEID&lt;/span&gt;})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;AGI&lt;/span&gt;(&lt;span class="n"&gt;agi&lt;/span&gt;://&lt;span class="n"&gt;localhost&lt;/span&gt;/&lt;span class="n"&gt;vicidial_incident_agi&lt;/span&gt;.&lt;span class="n"&gt;py&lt;/span&gt;,${&lt;span class="n"&gt;ARG1&lt;/span&gt;},${&lt;span class="n"&gt;ARG2&lt;/span&gt;},${&lt;span class="n"&gt;ARG3&lt;/span&gt;})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Return&lt;/span&gt;()

[&lt;span class="n"&gt;from&lt;/span&gt;-&lt;span class="n"&gt;vicidial&lt;/span&gt;-&lt;span class="n"&gt;inbound&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Inbound&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; ${&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;num&lt;/span&gt;)} &lt;span class="n"&gt;to&lt;/span&gt; ${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Macro&lt;/span&gt;(&lt;span class="n"&gt;incident&lt;/span&gt;-&lt;span class="n"&gt;hook&lt;/span&gt;,&lt;span class="n"&gt;ANSWER&lt;/span&gt;,${&lt;span class="n"&gt;TRUNK_NAME&lt;/span&gt;},${&lt;span class="n"&gt;CARRIER_ID&lt;/span&gt;})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;ViciDial&lt;/span&gt;(${&lt;span class="n"&gt;EXTEN&lt;/span&gt;},,,,,,,,)
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Macro&lt;/span&gt;(&lt;span class="n"&gt;incident&lt;/span&gt;-&lt;span class="n"&gt;hook&lt;/span&gt;,&lt;span class="n"&gt;HANGUP&lt;/span&gt;,${&lt;span class="n"&gt;TRUNK_NAME&lt;/span&gt;},${&lt;span class="n"&gt;CARRIER_ID&lt;/span&gt;})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On inbound call answer, the AGI is triggered and will check for any codec or timeout issues in the last 100 lines of the Asterisk log. If problems are detected, it queries the KB and calls Claude.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing SIP codec mismatches
&lt;/h2&gt;

&lt;p&gt;The most common incident is CODEC MISMATCH. To verify it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peers"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"UNREACHABLE|OK"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip set debug on"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then place a test call and watch the console. Look for lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2025-02-14 14:32:10] VERBOSE: channel.c: Channel SIP/carrier-00000001 answered SIP/agent-6001-00000002
[2025-02-14 14:32:11] WARNING: chan_sip.c: Codec mismatch on channel SIP/carrier-00000001: ULAW vs ALAW
[2025-02-14 14:32:42] WARNING: chan_sip.c: RTP timeout on channel SIP/carrier-00000001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to force transcoding in sip-vicidial.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="n"&gt;trunk&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="m"&gt;203&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;113&lt;/span&gt;.&lt;span class="m"&gt;50&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt;=&lt;span class="m"&gt;5060&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;
&lt;span class="n"&gt;directmedia&lt;/span&gt;=&lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;rtpkeepalive&lt;/span&gt;=&lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set directmedia=no to force audio through the Asterisk server (transcoding). This adds CPU load but prevents one-way audio when codecs differ. If the carrier supports only uLaw but the agent phone is aLaw, Asterisk will transcode in the middle.&lt;/p&gt;

&lt;p&gt;Restart the SIP channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peers"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;carrier-trunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resolving RTP timeouts from firewall rules
&lt;/h2&gt;

&lt;p&gt;RTP audio uses UDP ports 10000-20000 by default. If your firewall rule for those ports drifts (perhaps after a cloud migration or security patch), calls will drop after 30-45 seconds.&lt;/p&gt;

&lt;p&gt;Check iptables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"10000|20000|RTP"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; udp &lt;span class="nt"&gt;--dport&lt;/span&gt; 10000:20000 &lt;span class="nt"&gt;-s&lt;/span&gt; 203.0.113.0/24 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-p&lt;/span&gt; udp &lt;span class="nt"&gt;--dport&lt;/span&gt; 10000:20000 &lt;span class="nt"&gt;-d&lt;/span&gt; 203.0.113.0/24 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check if RTP keepalive is set in your SIP trunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="n"&gt;trunk&lt;/span&gt;]
&lt;span class="n"&gt;rtpkeepalive&lt;/span&gt;=&lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends an RTP keepalive packet every 20 seconds to prevent the firewall from timing out the session.&lt;/p&gt;

&lt;p&gt;Test RTP connectivity to the carrier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mtr &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; 203.0.113.50 &lt;span class="nt"&gt;-p&lt;/span&gt; 10000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Auto-fixing call state corruption
&lt;/h2&gt;

&lt;p&gt;Call state corruption happens when an agent's call is marked DONE in the vicidial_log but the agent is still in the queue. This freezes the agent until a manual intervention or log rotation.&lt;/p&gt;

&lt;p&gt;To detect this, query the log table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;uniqueid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'QUEUE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IN-CALL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'RING'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any call marked QUEUE or IN-CALL that is older than 5 minutes is stale. Fix it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'DONE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;uniqueid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'1707916330.1'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'DONE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large-scale cleanup, use this AGI approach in the vicidial_agent_inbound context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Check&lt;/span&gt; &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;)
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;STALE_CALL&lt;/span&gt;=${&lt;span class="n"&gt;SHELL&lt;/span&gt;(/&lt;span class="n"&gt;usr&lt;/span&gt;/&lt;span class="n"&gt;local&lt;/span&gt;/&lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;check_stale_calls&lt;/span&gt;.&lt;span class="n"&gt;sh&lt;/span&gt; ${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;GotoIf&lt;/span&gt;($[&lt;span class="s2"&gt;"${STALE_CALL}"&lt;/span&gt; != &lt;span class="s2"&gt;""&lt;/span&gt;]?&lt;span class="n"&gt;cleanup&lt;/span&gt;:&lt;span class="n"&gt;continue&lt;/span&gt;)
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;(&lt;span class="n"&gt;cleanup&lt;/span&gt;),&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;status&lt;/span&gt;=&lt;span class="n"&gt;DONE&lt;/span&gt;)
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Cleaned&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;: ${&lt;span class="n"&gt;STALE_CALL&lt;/span&gt;})
&lt;span class="n"&gt;same&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;n&lt;/span&gt;(&lt;span class="n"&gt;continue&lt;/span&gt;),&lt;span class="n"&gt;Return&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Querying the incident log for patterns
&lt;/h2&gt;

&lt;p&gt;Once Claude has logged incidents, you can spot patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DATE_FORMAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d %H:%i'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_incident_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DATE_FORMAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'%Y-%m-%d %H:%i'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If CODEC_MISMATCH spikes every morning at 08:00, that is when a batch job resets the SIP trunk to default settings. Check your cron jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"asterisk|sip"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude API returns "Invalid API key" error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check that CLAUDE_API_KEY is exported to the AGI process. Edit /etc/asterisk/asterisk.conf and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;files&lt;/span&gt;]
&lt;span class="n"&gt;astdatadir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astvardir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astdbdir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astkeydir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astdictdir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astmoddir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;usr&lt;/span&gt;/&lt;span class="n"&gt;lib64&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;modules&lt;/span&gt;
&lt;span class="n"&gt;astlibdir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;usr&lt;/span&gt;/&lt;span class="n"&gt;lib64&lt;/span&gt;
&lt;span class="n"&gt;astagidir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;usr&lt;/span&gt;/&lt;span class="n"&gt;lib&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;agi&lt;/span&gt;-&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="n"&gt;astspooldir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;spool&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astrundir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;run&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astlogdir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;
&lt;span class="n"&gt;astetcdir&lt;/span&gt; =&amp;gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;

[&lt;span class="n"&gt;startup&lt;/span&gt;]
; &lt;span class="n"&gt;Ensure&lt;/span&gt; &lt;span class="n"&gt;CLAUDE_API_KEY&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;AGI&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="n"&gt;processes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually, environment variables do not automatically inherit into AGI processes. Instead, source the key directly in the Python script or store it in a config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;configparser&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/vicidial/incident.conf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CLAUDE_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AGI script hangs or times out.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Asterisk AGI calls are synchronous and block the dialplan. If Claude takes longer than 10 seconds to respond, Asterisk will time out. Set a shorter timeout in the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.anthropic.com/v1/messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;  &lt;span class="c1"&gt;# Reduced from 10
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, spawn the Claude call asynchronously in a background job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;claude_call &amp;amp;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;# Run in background; do not block dialplan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MySQL connection drops mid-incident.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AGI script does not reconnect if the DB connection is lost. Add a reconnect handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictCursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OperationalError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MySQLdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DictCursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Asterisk log file is too large and rotates mid-event.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tail_asterisk_log function reads from a live file. If logrotate runs while the AGI is running, it will get only partial logs. Use a named pipe or journalctl instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tail_asterisk_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;journalctl -u asterisk -n &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; --no-pager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Journal read failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One-way audio persists after directmedia=no is set.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;directmedia=no only forces transcoding if Asterisk can reach both peers. If the agent phone is behind a strict NAT, Asterisk may still not be able to transcode. Check the agent's phone settings: it must send its real IP to Asterisk (register with its public IP or use a proxy). Alternatively, set nat=force_rport in the peer definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;agent&lt;/span&gt;-&lt;span class="n"&gt;phone&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;friend&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;=&lt;span class="n"&gt;force_rport&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does ViciDial mark calls as DEAD?
&lt;/h3&gt;

&lt;p&gt;Calls are marked DEAD in the vicidial_log when the call ends abnormally: disconnect is initiated by the carrier, not the agent. Check if the carrier is hanging up due to timeouts or invalid SIP headers. Run asterisk -rx "sip set debug on" to see CANCEL or BYE frames. If the carrier is sending BYE frames early, verify rtpkeepalive is set and check packet loss on the RTP port range.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Claude auto-remediate without breaking things?
&lt;/h3&gt;

&lt;p&gt;Yes, but only for idempotent actions: reloading a SIP trunk, resetting call state in the DB, or forcing a codec. Do not let Claude execute shell commands that affect running calls or delete records without approval. Define a whitelist of safe commands in the script and reject anything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the cost of running Claude per call?
&lt;/h3&gt;

&lt;p&gt;Each Claude API call costs roughly $0.001 (using claude-3-sonnet). If you trigger Claude on every inbound call, that is $100 per 100,000 calls. Instead, trigger only on detected problems: codec mismatches, timeouts, or unreachable carriers. This reduces the rate to one call per 50-100 incidents, bringing cost down to $0.02-$0.05 per incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent the AGI from blocking inbound calls?
&lt;/h3&gt;

&lt;p&gt;AGI calls are blocking by default. Run Claude calls in a background job or a separate daemon that listens to the incident log table. Use a Bash job with &amp;amp; or spawn a separate Python process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/vicidial_incident_agi.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens if the Claude API is down?
&lt;/h3&gt;

&lt;p&gt;The script has a 10-second timeout and a fallback: if Claude does not respond, log the incident as unanalyzed and continue the call. The on-call engineer can later query the raw incident log and run analysis manually. Add monitoring for Claude API availability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.anthropic.com/v1/messages &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"claude-3-sonnet-20250229","max_tokens":1,"messages":[{"role":"user","content":"ok"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.error'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Claude OK"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Claude DOWN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this check every 5 minutes via cron and page the oncall engineer if it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You have built an incident detection system for ViciDial that logs call events to MySQL, queries a curated knowledge base, and invokes Claude to analyze SIP and RTP failures. The key configuration points are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Created vicidial_incident_kb and vicidial_incident_log tables to store known issues and live incident data.&lt;/li&gt;
&lt;li&gt;Wrote a Python AGI script that logs call state and calls Claude with structured prompts on detected problems.&lt;/li&gt;
&lt;li&gt;Hooked the AGI into the dialplan with Macro(incident-hook) on inbound call events.&lt;/li&gt;
&lt;li&gt;Configured SIP trunks with directmedia=no and rtpkeepalive=20 to prevent codec and RTP timeout issues.&lt;/li&gt;
&lt;li&gt;Verified MySQL connectivity and Claude API error handling to prevent blocking the call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next steps are to run a week of test calls in production, collect real incident data, and add the most common patterns to vicidial_incident_kb. Monitor Claude response latency and adjust the timeout. Set up a dashboard to visualize incident types and resolution times, then iterate on the KB to improve Claude's accuracy. After two weeks, you should have enough data to hand off routine incidents to Claude-driven auto-remediation with human approval workflows for risky changes.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>ViciDial LAGGED agents: root cause analysis and debugging</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Sun, 18 Oct 2026 03:30:08 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/vicidial-lagged-agents-root-cause-analysis-and-debugging-3a4n</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/vicidial-lagged-agents-root-cause-analysis-and-debugging-3a4n</guid>
      <description>&lt;h1&gt;
  
  
  ViciDial LAGGED agents: root cause analysis and debugging
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Agents stuck in LAGGED status show no calls, drop inbound traffic, and fail to reconnect: identify the socket, session, and database state causing the freeze in under five minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LAGGED status means the agent's web session has disconnected from the Asterisk realtime socket or the ViciDial telephony daemon (ViciDial_Agent.pl) has lost the agent's active record. The agent appears logged in but receives no calls. The fix depends on which layer broke: kill the hung PHP session, restart the daemon, force a database update, or rebuild the SIP channel registration. Most LAGGED events trace to session timeout, daemon memory leak, or a stale vicidial_live_agents row with no matching active call context in Asterisk.&lt;/p&gt;

&lt;p&gt;Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, with Apache 2.4 and PHP 7.4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SSH access to the ViciDial server with sudoers privilege or root&lt;/li&gt;
&lt;li&gt;Asterisk CLI installed and permission to run asterisk -rx commands&lt;/li&gt;
&lt;li&gt;MySQL/MariaDB client and direct database access&lt;/li&gt;
&lt;li&gt;Familiarity with asterisk -vvv debugging output and SIP traces&lt;/li&gt;
&lt;li&gt;At least one agent account with active LAGGED status to test&lt;/li&gt;
&lt;li&gt;ViciDial web admin access at /vicidial/admin.php or higher&lt;/li&gt;
&lt;li&gt;Basic understanding of ViciDial agent states: READY, PAUSED, LAGGED, INCALL&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What "LAGGED" means in ViciDial
&lt;/h2&gt;

&lt;p&gt;An agent in LAGGED status has lost the real-time bidirectional link between their web browser (the agent screen) and the ViciDial backend processes. The agent's record exists in the vicidial_live_agents table, and their PHP session file still exists on disk, but the websocket or TCP connection that carries call events, caller ID, and disposition codes has timed out or been killed.&lt;/p&gt;

&lt;p&gt;When LAGGED, the agent sees no incoming calls in the dialpad, cannot click buttons to accept or reject calls, and the admin reports show no active calls for that extension. Meanwhile, the ViciDial_Agent.pl daemon cannot locate the agent's session state and either ignores new inbound calls or routes them to a fallback destination (voicemail, IVR, or another agent).&lt;/p&gt;

&lt;p&gt;The customer experience: dial an advertised number, wait on hold, hear silence, and disconnect when the LAGGED agent's queue times out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites for investigation
&lt;/h2&gt;

&lt;p&gt;Before you start, gather:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent's vicidial_user account name and assigned extension&lt;/li&gt;
&lt;li&gt;The timestamp when LAGGED was first observed (from web admin or agent report)&lt;/li&gt;
&lt;li&gt;The server hostname and any load balancer address&lt;/li&gt;
&lt;li&gt;Recent /var/log/asterisk/messages lines around that time&lt;/li&gt;
&lt;li&gt;The agent's assigned campaign, dial method, and call list&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to identify which agents are LAGGED
&lt;/h2&gt;

&lt;p&gt;Log into the ViciDial admin web interface at /vicidial/admin.php (or direct to /vicidial/admin/users.php). In the "Active Agents" or "Realtime Agents" panel, any agent showing LAGGED in the "Status" column is disconnected.&lt;/p&gt;

&lt;p&gt;Alternatively, query the database directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calls_today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_live_agents&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'LAGGED'&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;last_update&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last_update timestamp tells you how long they have been stuck. If last_update is more than 5 minutes old, the connection was lost well before the current time.&lt;/p&gt;

&lt;p&gt;Also check for orphaned SIP channels on the Asterisk side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channels"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"SIP/vicidial"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see SIP/vicidial-XXXX channels with no matching agent in vicidial_live_agents or with a LAGGED status, that channel is orphaned and likely wasting memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: PHP session and web connection diagnostics
&lt;/h2&gt;

&lt;p&gt;The first layer to investigate is the agent's web session (the PHP session file on disk and the HTTP/WebSocket connection to the browser).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do PHP sessions expire and trigger LAGGED status?
&lt;/h3&gt;

&lt;p&gt;A PHP session is a file stored on disk (usually in /var/lib/php/sessions or /tmp) that holds the agent's login state, assigned call lists, and campaign parameters. ViciDial web scripts check this file on every page load. If the session file is missing or corrupted, or if the browser tab closes without sending a "log out" message, the next refresh will prompt a re-login.&lt;/p&gt;

&lt;p&gt;Some agent browsers sit idle. After 10 to 30 minutes with no requests, Apache or PHP may close the connection. When the agent tries to click a button or open a new screen, the request fails because there is no active session. The ViciDial backend then receives no updates and assumes the agent has disconnected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check for active PHP sessions for a specific user
&lt;/h3&gt;

&lt;p&gt;SSH to the ViciDial server. List the session files for the target user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/lib/php/sessions/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; vicidial_user /var/www/html/vicidial.php 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'__'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A more direct approach: query the process list to find the agent's browser session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps auxww | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"[a]gent"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for PHP-CGI or Apache worker processes labeled with the agent's user account or session ID.&lt;/p&gt;

&lt;p&gt;To dump the session file and check its contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/lib/php/sessions/sess_&amp;lt;SESSION_ID_HERE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see serialized PHP data. Check that vicidial_user, user_id, and extension are present. If the file is empty or contains only a timestamp, the session is dead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Force an agent to re-login by clearing their session
&lt;/h3&gt;

&lt;p&gt;If you determine a session is corrupted, delete it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/lib/php/sessions/sess_&amp;lt;SESSION_ID_HERE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell the agent to refresh their browser. They will be asked to log in again. Once logged in, a new session file will be created and the agent should no longer show LAGGED.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: ViciDial daemon socket and memory state
&lt;/h2&gt;

&lt;p&gt;The second layer is the ViciDial_Agent.pl daemon, which runs on the server and holds an in-memory copy of every active agent's state: their status (READY, PAUSED, INCALL), assigned phone, SIP extension, and the file descriptor for their socket connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the ViciDial daemon have the agent in memory?
&lt;/h3&gt;

&lt;p&gt;Connect to the ViciDial admin port (default 6100, configurable in /etc/asterisk/asterisk.conf or vicidial.conf) using telnet or a custom admin tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;telnet localhost 6100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get a ViciDial command prompt. Use the STATUS command to list all active agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATUS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press Enter. The daemon will print a line for each agent in memory. Look for your target agent's extension or username. If you do not see it, the daemon has no record of that agent, and the agent must re-login to be re-registered.&lt;/p&gt;

&lt;p&gt;If the agent appears in the STATUS output, note their assigned handler process ID and socket number. This tells you the daemon thinks they are still active.&lt;/p&gt;

&lt;p&gt;Exit telnet by typing QUIT or Ctrl+D.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the ViciDial daemon logs
&lt;/h3&gt;

&lt;p&gt;The ViciDial_Agent.pl daemon logs to a dedicated file, usually /var/log/ViciDial_Agent.log or /var/log/asterisk/ViciDial_Agent.log (depends on the installation). Tail the last 100 lines and search for the agent's extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-100&lt;/span&gt; /var/log/ViciDial_Agent.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"extension_XXXX"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for socket timeout warnings or "closing connection" messages near the timestamp when the agent went LAGGED.&lt;/p&gt;

&lt;p&gt;If you see messages like "Socket timeout on extension XXXX after 300 seconds", the daemon closed the connection because no keep-alive packet arrived from the browser in 5 minutes. This is a sign that the browser is not polling the server or has lost network connectivity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restart the ViciDial daemon
&lt;/h3&gt;

&lt;p&gt;If the daemon is consuming excessive memory or has accumulated stale agent records, restart it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ViciDial_Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /etc/init.d/ViciDial_Agent restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restarting will drop all agent sessions. All agents logged in will be disconnected and must log back in. Coordinate this restart during a low-traffic period.&lt;/p&gt;

&lt;p&gt;To restart without killing agents: edit the daemon config to reduce socket timeout from 300 seconds to 30 seconds, then tell the daemon to reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo kill&lt;/span&gt; &lt;span class="nt"&gt;-HUP&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pidof ViciDial_Agent.pl&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the daemon to reread config. However, this does not drop existing agent connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: ViciDial_live_agents database state
&lt;/h2&gt;

&lt;p&gt;The third layer is the vicidial_live_agents table, which is the source of truth for which agents are currently logged in. Each row represents one active agent session. When an agent logs in, a row is inserted. When they log out, the row is deleted. If the row exists but the corresponding daemon socket and PHP session are gone, the agent is LAGGED.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the agent row in vicidial_live_agents
&lt;/h3&gt;

&lt;p&gt;Query the table for the agent in question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;vicidial_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;calls_today&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;server_ip&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_live_agents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AGENT_USERNAME_HERE'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status: should be READY, PAUSED, or INCALL if the agent is healthy. LAGGED means the web connection is stale.&lt;/li&gt;
&lt;li&gt;last_update: should be within the last 10 seconds. If it is more than 5 minutes old, the agent's web browser has not refreshed in that time.&lt;/li&gt;
&lt;li&gt;session_id: should match the PHP session ID currently stored in /var/lib/php/sessions/sess_.&lt;/li&gt;
&lt;li&gt;server_ip: should be the IP of the ViciDial server or the agent's client IP if behind a load balancer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Force-update the agent to PAUSED to clear LAGGED
&lt;/h3&gt;

&lt;p&gt;If the database row is stale but the agent is still in the browser, you can force the status to PAUSED without deleting the row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_live_agents&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PAUSED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AGENT_USERNAME_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's browser should detect the change and refresh their dialpad. If they remain LAGGED after this, the browser has no active connection to the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete the agent row to force re-login
&lt;/h3&gt;

&lt;p&gt;If the agent is truly stuck, delete their row from vicidial_live_agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_live_agents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AGENT_USERNAME_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent will be logged out immediately. Have them log back in. A new row will be inserted, a new PHP session created, and the daemon will re-register them.&lt;/p&gt;

&lt;p&gt;Be careful: if the agent is currently on a call, deleting their row will orphan the call and may charge you for a call that never completed properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Asterisk SIP channel and state
&lt;/h2&gt;

&lt;p&gt;The fourth layer is Asterisk itself and the SIP channels registered for the agent's extension.&lt;/p&gt;

&lt;h3&gt;
  
  
  List SIP channels for an agent extension
&lt;/h3&gt;

&lt;p&gt;Ask for all active SIP channels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channels"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output will resemble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Peer             User/ANR    Call ID              Duration Recv-Q Lost %   Jitter FromTag
SIP/1001-00000ab agent1      sip:1001@10.0.0.1    00:02:35    0    0    0.0   12345abc
SIP/1002-00000ac agent2      sip:1002@10.0.0.1    00:00:15    0    0    0.0   67890def
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the agent's extension (e.g., 1001). If you see one channel, they are logged in and Asterisk knows about them. If you see zero or multiple stale channels, that is a clue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the SIP peer registration for the agent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peers"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"1001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the peer is listed as "OK" with a recent "Refresh" timestamp, the registration is active. If it shows "UNREACHABLE" or has not refreshed in more than 5 minutes, the phone or endpoint is not responding to SIP REGISTER packets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dump a SIP channel state
&lt;/h3&gt;

&lt;p&gt;If you need to see detailed state for a specific channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channel SIP/1001-00000ab"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will print codec negotiation, RTP ports, and any pending transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Force Asterisk to reload SIP config
&lt;/h3&gt;

&lt;p&gt;If you edited sip-vicidial.conf or extensions-vicidial.conf, reload without restarting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"dialplan reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reloading will drop any active SIP channels. Agents will need to re-register.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: Real-time state via Asterisk AMI and ViciDial logs
&lt;/h2&gt;

&lt;p&gt;The fifth layer is the Asterisk Manager Interface (AMI), which is the real-time API that ViciDial uses to monitor call state. If AMI is not connected or is lagging, the ViciDial daemon cannot see call events and will mark agents as LAGGED incorrectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check if AMI is running and connected
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"manager list connected"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see lines listing connected clients. Look for "vicidial" or "ViciDial" in the client list. If there are zero connections or the connection is very old, the daemon is not talking to Asterisk.&lt;/p&gt;

&lt;p&gt;Restart the AMI connection by restarting the daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ViciDial_Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tail the Asterisk full log for AMI errors
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/asterisk/full | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"ami&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;manager"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for connection refused, socket timeout, or authentication error messages. If AMI failed to authenticate, check the manager.conf file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/asterisk/manager.conf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;[vicidial&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure the username and password match what is configured in the ViciDial daemon (usually in /etc/asterisk/vicidial.conf or hard-coded in ViciDial_Agent.pl). If they do not match, the daemon cannot connect and all agents will eventually go LAGGED.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Symptom: Agent goes LAGGED within 2 minutes of login
&lt;/h3&gt;

&lt;p&gt;Check the Apache access and error logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/apache2/access.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"vicidial.php"&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/apache2/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see 502 Bad Gateway or 503 Service Unavailable, the PHP backend is crashing or overloaded. Restart Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If PHP crashes again immediately, increase the memory limit in /etc/php/7.4/cgi/php.ini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;memory_limit&lt;/span&gt; = &lt;span class="m"&gt;512&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Apache and test again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: All agents are LAGGED after a reboot
&lt;/h3&gt;

&lt;p&gt;The ViciDial daemon may not have started. Check if it is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps auxww | &lt;span class="nb"&gt;grep &lt;/span&gt;ViciDial_Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If not running, start it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start ViciDial_Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails to start, check the daemon log for startup errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt; /var/log/ViciDial_Agent.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common startup errors: MySQL connection refused (check if MySQL is running), missing Perl modules (run CPAN or apt install), or stale PID file. Delete a stale PID and retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/run/ViciDial_Agent.pid
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start ViciDial_Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Symptom: Agents are LAGGED but PhoneType is listed as "NONE"
&lt;/h3&gt;

&lt;p&gt;Check the vicidial_users table. The user may not have a phone device assigned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone_number&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AGENT_USERNAME_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If phone_type is "NONE" or NULL, the agent cannot register a SIP extension. Edit the user record in the web admin and assign a phone type (typically "SIP" for soft phones). Reload SIP and have the agent re-login.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: LAGGED agents reappear when the web server restarts
&lt;/h3&gt;

&lt;p&gt;Check if /var/lib/php/sessions has sufficient disk space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /var/lib/php/sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If free space is less than 5%, sessions may be getting garbage-collected. Clean up old session files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find /var/lib/php/sessions &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-mtime&lt;/span&gt; +7 &lt;span class="nt"&gt;-delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then expand the session storage device if possible. Also increase PHP session.gc_maxlifetime in php.ini to match ViciDial's timeout (usually 14400 seconds, 4 hours):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt;.&lt;span class="n"&gt;gc_maxlifetime&lt;/span&gt; = &lt;span class="m"&gt;14400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Symptom: One or two agents are LAGGED, but most others are fine
&lt;/h3&gt;

&lt;p&gt;This is usually a network issue on the agent's side. Have the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reload the browser tab (Ctrl+F5 or Cmd+Shift+R to bypass cache).&lt;/li&gt;
&lt;li&gt;Check their local IP and firewall. Ping the server.&lt;/li&gt;
&lt;li&gt;Try a different browser or device if possible.&lt;/li&gt;
&lt;li&gt;Check if they are on a corporate proxy or VPN that is blocking websockets or long-polling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the issue persists only for that agent, restart their session in the database as described in "Force an agent to re-login by clearing their session" above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does an agent see READY status in the dialpad but show LAGGED in the admin console?
&lt;/h3&gt;

&lt;p&gt;The agent's PHP session is active, so their browser shows READY because it has a cached version of the status. However, the last_update timestamp in vicidial_live_agents is stale, so the admin console shows LAGGED. The web connection is asymmetrical: the browser is still making requests (keeping PHP alive), but the server is not receiving websocket push events or the polling frequency has dropped so low that real-time synchronization is lost. Force a refresh of the agent screen (Ctrl+F5) to resync. If the issue persists, delete the database row and have the agent re-login.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I write a cron job to auto-clear LAGGED agents?
&lt;/h3&gt;

&lt;p&gt;Yes, but do so cautiously to avoid interrupting real calls. Create a cron script that runs every 5 minutes and updates any agent whose last_update is older than 5 minutes to PAUSED, then check again after 10 minutes to see if they reconnect. If they remain PAUSED after 15 minutes, delete the row. Example shell loop (place in /usr/local/bin/clear_lagged.sh):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
mysql &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"UPDATE vicidial_live_agents SET status = 'PAUSED' WHERE status = 'LAGGED' AND last_update &amp;lt; DATE_SUB(NOW(), INTERVAL 5 MINUTE);"&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;600
mysql &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"DELETE FROM vicidial_live_agents WHERE status = 'PAUSED' AND last_update &amp;lt; DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND calls_today = 0;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule it with crontab -e:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt;/10 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/clear_lagged.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs every 10 minutes. Adjust the 5 and 15 minute thresholds to suit your environment. Always test in a staging environment first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is LAGGED the same as a dropped call or a no-answer?
&lt;/h3&gt;

&lt;p&gt;No. LAGGED is an agent state, not a call result. A call can have a LAGGED agent and still be transferred, dropped, or forwarded depending on the dial plan. A LAGGED agent cannot answer new calls or adjust their status, but calls already connected to that extension will remain connected until one party hangs up. However, new inbound calls will not ring that agent, so they will not see them in their dialpad and cannot click to accept them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What causes LAGGED if the agent is sitting idle in the dialpad?
&lt;/h3&gt;

&lt;p&gt;Agents who do not interact with the web page for more than 10 minutes typically lose their polling connection. The browser is still running, but no new HTTP requests are being sent, so the server's keep-alive timer expires and the backend marks them LAGGED. To prevent this, configure a JavaScript keep-alive in the dialpad (ViciDial usually includes this), or reduce the polling timeout in the browser settings. Some deployments use websockets instead of polling to avoid this, but websockets require additional server-side support and can consume more memory per agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  If I delete a LAGGED agent row from the database, what happens to their active call?
&lt;/h3&gt;

&lt;p&gt;If the agent is not actually on an active call, nothing happens. If they are on a call (lead_id is not null), the call remains connected at the Asterisk level, but the agent's record is gone. The call will not be logged properly in vicidial_log because the agent record is not there to supply user_id and other context. The call will show up as orphaned or with a null user_id. Always check the lead_id field before deleting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_live_agents&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vicidial_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AGENT_USERNAME_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If lead_id is a positive number, the agent is on a call. In that case, do not delete the row. Instead, try the force-update to PAUSED method first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A LAGGED agent is disconnected at the web session, daemon socket, or database level, despite an apparently logged-in record. Investigation starts with the PHP session file and browser connectivity, then moves to the ViciDial daemon's in-memory state and the vicidial_live_agents table, and finally checks Asterisk SIP registration and AMI connectivity.&lt;/p&gt;

&lt;p&gt;Standard fixes are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delete the agent's PHP session file and ask them to re-login.&lt;/li&gt;
&lt;li&gt;Restart the ViciDial daemon if it is consuming excessive memory or has lost AMI connection.&lt;/li&gt;
&lt;li&gt;Update or delete the stale row in vicidial_live_agents.&lt;/li&gt;
&lt;li&gt;Reload SIP configuration if the agent extension is not registering properly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To avoid LAGGED in the future: reduce session timeout in PHP and the daemon to match your environment, use keep-alive pings in the dialpad, monitor vicidial_live_agents for stale rows, and ensure the daemon can always reach Asterisk AMI without blocking. Regular log review of /var/log/ViciDial_Agent.log and /var/log/asterisk/messages will catch most issues before they affect more than one agent.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>Homer SIP Capture &amp; RTCP Monitoring for Asterisk/ViciDial</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Sat, 17 Oct 2026 03:30:03 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/homer-sip-capture-rtcp-monitoring-for-asteriskvicidial-3o2b</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/homer-sip-capture-rtcp-monitoring-for-asteriskvicidial-3o2b</guid>
      <description>&lt;h1&gt;
  
  
  Homer SIP Capture &amp;amp; RTCP Monitoring for Asterisk/ViciDial
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Capture every SIP packet and RTP stream from Asterisk: detect call quality failures, codec mismatches, and dropped audio before agents report silent calls.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Homer HEP (Homer Encapsulation Protocol) captures all SIP signaling and RTP statistics from your Asterisk/ViciDial system and streams them to a centralized Homer UI for real-time and historical analysis. Silent calls, one-way audio, and jitter-induced dropouts become visible in packet detail within seconds of occurrence, eliminating the guesswork of call tracing through Asterisk logs alone. This tutorial walks through installing Homer Server, configuring Asterisk HEP forwarding, tuning RTCP metrics, and querying call data to pinpoint carrier failures, codec negotiation errors, and network problems in your ViciDial infrastructure.&lt;/p&gt;

&lt;p&gt;Tested on ViciDial 2.14 (SVN 3555+), Asterisk 16/18, Homer 7.8+, MariaDB 10.5+, CentOS 7/8 and Ubuntu 18.04 LTS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A working Asterisk/ViciDial installation with outbound calling enabled.&lt;/li&gt;
&lt;li&gt;Root or sudo access on both the ViciDial server and a dedicated Homer server (or co-located).&lt;/li&gt;
&lt;li&gt;Network connectivity between Asterisk and Homer (UDP port 6060 and TCP 9060 typical).&lt;/li&gt;
&lt;li&gt;MariaDB or MySQL 10.5+ with at least 50 GB free for call history (HEP packets consume 200-500 KB per call).&lt;/li&gt;
&lt;li&gt;Familiarity with Asterisk CLI commands and basic ViciDial diagnostics.&lt;/li&gt;
&lt;li&gt;Optional but recommended: a second NIC or VLAN on the ViciDial box to isolate HEP traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Homer and why ViciDial needs it
&lt;/h2&gt;

&lt;p&gt;Homer is a SIP capture and analytics platform that records every SIP message (INVITE, 100 Trying, 200 OK, BYE, etc.) and RTP stream statistics in real time. When an agent reports a silent call or a customer hears only their own voice, Homer's packet graph and RTP metrics show you whether the problem was on your network, your carrier, or your codec selection.&lt;/p&gt;

&lt;p&gt;ViciDial's built-in call logging and Asterisk CLI are good for counting calls and marking outcomes, but they do not capture packet-level detail. A call can complete successfully in the Asterisk database yet still have one-way audio because of a firewall rule, a carrier SDP mismatch, or an RTP timeout. Homer fills that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;Your Asterisk server runs a small HEP forwarder (either the Asterisk &lt;code&gt;chan_sip&lt;/code&gt; or &lt;code&gt;chan_pjsip&lt;/code&gt; module, or a dedicated sniffer like &lt;code&gt;hepic&lt;/code&gt;). This forwarder sends a copy of every SIP packet and RTP packet to Homer's HEP collector (default port 6060 UDP). Homer decodes the packets, stores them in a database, and serves a web UI where you search by call ID, phone number, time range, and result code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Asterisk (Outbound Call)
    |
    +---&amp;gt; HEP Forwarder (SIP module + hepic or VQ modules)
             |
             +---&amp;gt; Homer HEP Collector (UDP 6060)
                       |
                       +---&amp;gt; Homer DB (MySQL/MariaDB)
                       |
                       +---&amp;gt; Homer UI (HTTP 9060+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a ViciDial cluster, you can forward from multiple Asterisk boxes to a single Homer server, then correlate calls across the cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Install Homer Server
&lt;/h2&gt;

&lt;p&gt;Homer runs on a dedicated Linux box or a non-Asterisk server in your network. We assume CentOS 7 or Ubuntu 18.04 LTS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install dependencies
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CentOS 7:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; epel-release
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; MariaDB-server MariaDB-client git golang wget curl
systemctl start mariadb
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ubuntu 18.04 LTS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; mariadb-server mariadb-client git golang-go wget curl
systemctl start mariadb
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create Homer database and user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'EOF'&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;homer_data&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;CHARACTER&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;utf8mb4&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;utf8mb4_unicode_ci&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;homer_config&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;CHARACTER&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;utf8mb4&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;utf8mb4_unicode_ci&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'homer'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'homer_password_123'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;homer_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'homer'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;homer_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'homer'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;EXIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;homer_password_123&lt;/code&gt; with a strong password and save it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clone Homer from GitHub and build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt
git clone https://github.com/sipcapture/homer.git
&lt;span class="nb"&gt;cd &lt;/span&gt;homer
git checkout 7.8  &lt;span class="c"&gt;# or latest stable tag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the README. For a quick test, use Docker (optional). For production on bare metal, follow the build instructions in the repo. The Go-based binaries are statically linked and need only a standard Linux kernel.&lt;/p&gt;

&lt;p&gt;Build the Homer API and capture server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/homer
make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;make&lt;/code&gt; is not available, download pre-built binaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/sipcapture/homer/releases/download/v7.8/homer-7.8-linux-amd64.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf homer-7.8-linux-amd64.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /opt/homer/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initialize Homer database schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/homer
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; homer &lt;span class="nt"&gt;-p&lt;/span&gt; homer_data &amp;lt; sql/homer_data.sql
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; homer &lt;span class="nt"&gt;-p&lt;/span&gt; homer_config &amp;lt; sql/homer_config.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create Homer configuration file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/homer/homer.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "hep_listen": "0.0.0.0:6060",
  "http_listen": "0.0.0.0:9060",
  "db_driver": "mysql",
  "db_dsn": "homer:homer_password_123@tcp(localhost:3306)/homer_data",
  "db_config": "homer:homer_password_123@tcp(localhost:3306)/homer_config",
  "loki_host": "",
  "redis_host": "",
  "debug": false,
  "cors": true,
  "auth_enabled": false,
  "max_packet_size": 65535,
  "table_rotation": "day"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/homer
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/homer/homer.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;auth_enabled&lt;/code&gt; to true and configure LDAP or local users in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start Homer services
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/homer
./homer-api &lt;span class="nt"&gt;-config&lt;/span&gt; /etc/homer/homer.conf &amp;amp;
./homer-capture &lt;span class="nt"&gt;-config&lt;/span&gt; /etc/homer/homer.conf &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/homer-api.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Homer API
After=network.target mariadb.service

[Service]
Type=simple
User=homer
ExecStart=/opt/homer/homer-api -config /etc/homer/homer.conf
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/homer-capture.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Homer HEP Capture
After=network.target mariadb.service

[Service]
Type=simple
User=homer
ExecStart=/opt/homer/homer-capture -config /etc/homer/homer.conf
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; homer 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; homer:homer /opt/homer /etc/homer
systemctl daemon-reload
systemctl start homer-api homer-capture
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;homer-api homer-capture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test that Homer is listening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'(6060|9060)'&lt;/span&gt;
curl http://localhost:9060
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the Homer UI homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Configure Asterisk to forward HEP
&lt;/h2&gt;

&lt;p&gt;Asterisk can forward SIP and RTP packets to Homer using either the native HEP module (for PJSIP) or a sniffer daemon. We cover both methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method A: Using chan_pjsip with HEP module (recommended for Asterisk 16+)
&lt;/h3&gt;

&lt;p&gt;First, check if your Asterisk has the HEP module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module show like hep"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;res_hep.so&lt;/code&gt; is listed and loaded, proceed. If not, rebuild Asterisk with &lt;code&gt;--enable-hep&lt;/code&gt; or load it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module load res_hep.so"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module load res_hep_pjsip.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now configure HEP in &lt;code&gt;/etc/asterisk/hep.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;hep&lt;/span&gt;]
&lt;span class="n"&gt;enabled&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt; = &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;100&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt; = &lt;span class="m"&gt;6060&lt;/span&gt;
&lt;span class="n"&gt;protocol&lt;/span&gt; = &lt;span class="n"&gt;udp&lt;/span&gt;
&lt;span class="n"&gt;capture_id&lt;/span&gt; = &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;uuid_type&lt;/span&gt; = &lt;span class="n"&gt;call&lt;/span&gt;-&lt;span class="n"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;192.168.1.100&lt;/code&gt; with your Homer server's IP. The &lt;code&gt;capture_id&lt;/code&gt; is used to tag packets from this Asterisk instance (useful when monitoring multiple Asterisk boxes).&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;/etc/asterisk/hep_pjsip.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;enabled&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;send_via_ip&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload Asterisk modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload res_hep.so"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload res_hep_pjsip.so"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"hep show status"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Method B: Using sip.conf with chan_sip (legacy, Asterisk 13-18)
&lt;/h3&gt;

&lt;p&gt;If you are still using chan_sip, enable SIP capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
; &lt;span class="n"&gt;In&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;sip&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;sip&lt;/span&gt;-&lt;span class="n"&gt;vicidial&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="n"&gt;sipdebug&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;CLI&lt;/span&gt;:
; &lt;span class="n"&gt;asterisk&lt;/span&gt; -&lt;span class="n"&gt;rx&lt;/span&gt; &lt;span class="s2"&gt;"sip set debug"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run a sniffer on the Asterisk box to forward captured packets to Homer. Use &lt;code&gt;hepic&lt;/code&gt; (Kamailio HEP Forwarder):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install hepic&lt;/span&gt;
wget https://github.com/sippy/hepic/releases/download/v1.0/hepic-linux-amd64
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x hepic-linux-amd64
&lt;span class="nb"&gt;mv &lt;/span&gt;hepic-linux-amd64 /usr/local/bin/hepic

&lt;span class="c"&gt;# Run it&lt;/span&gt;
/usr/local/bin/hepic &lt;span class="nt"&gt;-listen&lt;/span&gt; 0.0.0.0:5061 &lt;span class="nt"&gt;-connect&lt;/span&gt; 192.168.1.100:6060 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in sip.conf, set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;sipdebug&lt;/span&gt; = &lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run tcpdump to forward to hepic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; eth0 &lt;span class="nt"&gt;-nn&lt;/span&gt; &lt;span class="s1"&gt;'udp port 5060 or udp port 16384:32767'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; - | hepic &lt;span class="nt"&gt;-listen&lt;/span&gt; stdin &lt;span class="nt"&gt;-connect&lt;/span&gt; 192.168.1.100:6060
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is cumbersome. Method A (PJSIP + HEP module) is preferred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure RTCP to send to Homer
&lt;/h3&gt;

&lt;p&gt;RTP streams themselves are not captured by HEP, but RTCP reports (which contain jitter, packet loss, and latency stats) are. Ensure RTCP is enabled in pjsip.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;transport&lt;/span&gt;-&lt;span class="n"&gt;udp&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;transport&lt;/span&gt;
&lt;span class="n"&gt;protocol&lt;/span&gt; = &lt;span class="n"&gt;udp&lt;/span&gt;
&lt;span class="n"&gt;bind&lt;/span&gt; = &lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;:&lt;span class="m"&gt;5060&lt;/span&gt;
&lt;span class="n"&gt;rtcp_mux&lt;/span&gt; = &lt;span class="n"&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;rtcp_mux = no&lt;/code&gt; forces RTCP to use a separate port (RTP+1) instead of multiplexing. This makes packet capture cleaner for testing but uses slightly more bandwidth.&lt;/p&gt;

&lt;p&gt;Verify RTCP is flowing from the carrier side. In Homer's UI, after a call completes, search for the call ID and look for RTCP packets with types 200 (SR, Sender Report) or 201 (RR, Receiver Report).&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: Configure ViciDial for HEP
&lt;/h2&gt;

&lt;p&gt;ViciDial itself does not configure HEP. Instead, Asterisk (which ViciDial controls) does. However, you need to ensure ViciDial's outbound and inbound dialing use the correct SIP profiles that have HEP enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check your ViciDial carrier configuration
&lt;/h3&gt;

&lt;p&gt;In the web admin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/vicidial/admin.php
Admin &amp;gt; Carrier &amp;gt; Edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that your carriers point to a valid Asterisk SIP context and use a pjsip endpoint. Look at &lt;code&gt;/etc/asterisk/pjsip.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;carriers&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;aor&lt;/span&gt;
&lt;span class="n"&gt;max_contacts&lt;/span&gt; = &lt;span class="m"&gt;1&lt;/span&gt;

[&lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;](&lt;span class="n"&gt;carriers&lt;/span&gt;)
&lt;span class="n"&gt;contact&lt;/span&gt; = &lt;span class="n"&gt;sip&lt;/span&gt;:&lt;span class="n"&gt;gateway&lt;/span&gt;.&lt;span class="n"&gt;provider&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;:&lt;span class="m"&gt;5060&lt;/span&gt;

[&lt;span class="n"&gt;endpoint&lt;/span&gt;-&lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt; = &lt;span class="n"&gt;endpoint&lt;/span&gt;
&lt;span class="n"&gt;aor&lt;/span&gt; = &lt;span class="n"&gt;carrier&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;outbound_proxy&lt;/span&gt; = &lt;span class="n"&gt;sip&lt;/span&gt;:&lt;span class="n"&gt;gateway&lt;/span&gt;.&lt;span class="n"&gt;provider&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;:&lt;span class="m"&gt;5060&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt; = &lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;gsm&lt;/span&gt;
&lt;span class="n"&gt;dtmf_mode&lt;/span&gt; = &lt;span class="n"&gt;rfc4733&lt;/span&gt;
&lt;span class="n"&gt;from_user&lt;/span&gt; = &lt;span class="n"&gt;your_did&lt;/span&gt;
&lt;span class="n"&gt;from_domain&lt;/span&gt; = &lt;span class="n"&gt;gateway&lt;/span&gt;.&lt;span class="n"&gt;provider&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HEP module will capture all SIP traffic on this endpoint automatically. No additional config in ViciDial is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify ViciDial logs do not interfere
&lt;/h3&gt;

&lt;p&gt;Asterisk verbose logging is resource-intensive. Disable high verbosity in Asterisk and rely on Homer for detailed call tracing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core set verbose 0"&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core set debug 0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, in &lt;code&gt;/etc/asterisk/logger.conf&lt;/code&gt;, log only errors and warnings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;logfiles&lt;/span&gt;]
&lt;span class="n"&gt;messages&lt;/span&gt; =&amp;gt; &lt;span class="n"&gt;notice&lt;/span&gt;,&lt;span class="n"&gt;warning&lt;/span&gt;,&lt;span class="n"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces disk I/O and keeps Asterisk fast while Homer captures everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: Query calls in Homer UI
&lt;/h2&gt;

&lt;p&gt;Open your browser to &lt;code&gt;http://homer-server:9060&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search by Call-ID
&lt;/h3&gt;

&lt;p&gt;Most VoIP calls carry a unique Call-ID in the SIP INVITE. To find a call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click "Search" or "Calls".&lt;/li&gt;
&lt;li&gt;Select time range (e.g., "Last 1 hour").&lt;/li&gt;
&lt;li&gt;Enter the caller number, callee number, or Call-ID.&lt;/li&gt;
&lt;li&gt;Click "Search".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Homer displays a list of calls. Click a call to see the SIP flow (INVITE, 100, 180, 200, ACK, BYE, etc.) and RTP statistics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interpret RTP and RTCP metrics
&lt;/h3&gt;

&lt;p&gt;When you drill into a call, Homer shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source / Dest IP:Port&lt;/strong&gt;: Where the RTP stream originated and terminated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codec&lt;/strong&gt;: Audio codec negotiated (PCMU, PCMA, GSM, Opus, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jitter&lt;/strong&gt;: Variance in packet arrival time (milliseconds). More than 50 ms is bad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packet Loss&lt;/strong&gt;: Percentage of RTP packets that did not arrive. Anything above 2% causes audible degradation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round Trip Time (RTT)&lt;/strong&gt;: Latency from Asterisk to the peer and back. Over 150 ms causes echo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MOS (Mean Opinion Score)&lt;/strong&gt;: Estimated audio quality (1-5 scale). 4+ is acceptable for customers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: A call shows codec PCMU, jitter 120 ms, 8% packet loss, MOS 2.1. This indicates a poor network path to the carrier gateway. Contact your ISP or carrier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export call data for analysis
&lt;/h3&gt;

&lt;p&gt;Click "Export" on the search results to download a CSV of calls. Use this to correlate one-way audio incidents with specific time windows, carriers, or agent IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: Troubleshoot common HEP issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Homer receives no packets
&lt;/h3&gt;

&lt;p&gt;Check that UDP 6060 is open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On Homer server&lt;/span&gt;
netstat &lt;span class="nt"&gt;-tlnpu&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;6060
&lt;span class="c"&gt;# Should show homer-capture listening on 0.0.0.0:6060&lt;/span&gt;

&lt;span class="c"&gt;# From Asterisk box, test connectivity&lt;/span&gt;
nc &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; 192.168.1.100 6060 &amp;lt; /dev/null
&lt;span class="c"&gt;# Should not timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If timeout, check firewall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On Homer server&lt;/span&gt;
iptables &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;6060
&lt;span class="c"&gt;# Add rule if missing:&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; udp &lt;span class="nt"&gt;--dport&lt;/span&gt; 6060 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Asterisk HEP module status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"hep show status"&lt;/span&gt;
&lt;span class="c"&gt;# Should show "HEP Enabled: Yes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If HEP is disabled, reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module reload res_hep"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  HEP works but no SIP packets appear
&lt;/h3&gt;

&lt;p&gt;Verify the capture ID in hep.conf matches what Homer expects. In Homer logs (&lt;code&gt;/var/log/homer/homer-capture.log&lt;/code&gt;), you should see packet counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/homer/homer-capture.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"packet"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no packets logged, Asterisk may not be sending them. Check that Asterisk is actually handling calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show calls"&lt;/span&gt;
&lt;span class="c"&gt;# or for PJSIP:&lt;/span&gt;
asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"pjsip list channels"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If calls are running but HEP is silent, the module may have failed to start. Restart Asterisk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart asterisk
systemisk status asterisk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  One-way audio appears in Homer but Asterisk logs show OK
&lt;/h3&gt;

&lt;p&gt;This is Homer's strength. The call succeeded in the Asterisk database (ViciDial marks it &lt;code&gt;CONNECTED&lt;/code&gt;) but Homer's RTP metrics reveal the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RTP from A to B is present, B to A is missing&lt;/strong&gt;: Check firewall rules on the B side (carrier gateway or far-end PBX). They may be blocking inbound RTP from your Asterisk IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both directions present but high jitter/loss on one direction&lt;/strong&gt;: The carrier's return path is congested. Ask them to check routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No RTCP reports from the far end&lt;/strong&gt;: The carrier is not sending RTCP, or it is blocked. Some cheap VoIP providers disable RTCP. This means you have no quality visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storage and retention
&lt;/h3&gt;

&lt;p&gt;Homer logs to MariaDB. By default, it keeps all packets. This grows large quickly. Set a retention policy in homer.conf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"table_rotation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"day"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"table_retention_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rotates tables daily and purges data older than 7 days. Adjust as needed. On a busy Asterisk handling 100+ concurrent calls, 7 days consumes 20-50 GB.&lt;/p&gt;

&lt;p&gt;For long-term storage, export older calls to a data warehouse or archive storage before purging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 6: Integrate Homer with ViciDial dashboards
&lt;/h2&gt;

&lt;p&gt;ViciDial does not have a built-in Homer integration, but you can create a custom link in the agent screen or admin interface to jump to Homer for a given call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Homer link to vicidial.php
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;/var/www/html/agc/vicidial.php&lt;/code&gt; (or your custom agent screen template). Near where call details are displayed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// After displaying call details, add:&lt;/span&gt;
&lt;span class="nv"&gt;$call_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^a-zA-Z0-9@.-]/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_REQUEST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'call_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// sanitize&lt;/span&gt;
&lt;span class="nv"&gt;$homer_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://192.168.1.100:9060/?query="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$call_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;a href='&lt;/span&gt;&lt;span class="nv"&gt;$homer_url&lt;/span&gt;&lt;span class="s2"&gt;' target='_blank' class='btn btn-info'&amp;gt;View RTP in Homer&amp;lt;/a&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This assumes ViciDial stores the call ID in the session or request. Adjust the variable name based on your ViciDial version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Homer link to vicidial_log
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;/vicidial/admin.php&lt;/code&gt;, edit the call log display to include a Homer link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// In call log listing:&lt;/span&gt;
&lt;span class="nv"&gt;$call_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'vicidial_id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// or whatever field holds the call ID&lt;/span&gt;
&lt;span class="nv"&gt;$homer_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;a href='http://192.168.1.100:9060/?query="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$call_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"' target='_blank'&amp;gt;View in Homer&amp;lt;/a&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$homer_link&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Asterisk crashes after enabling HEP
&lt;/h3&gt;

&lt;p&gt;If Asterisk becomes unstable after loading res_hep, the module may have a bug in your Asterisk version. Try disabling HEP and use the sniffer method (hepic) instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"module unload res_hep"&lt;/span&gt;
systemctl restart asterisk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run hepic separately as shown in Method B.&lt;/p&gt;

&lt;h3&gt;
  
  
  Homer UI loads but shows "No data available"
&lt;/h3&gt;

&lt;p&gt;Check that the database connection is valid. Test manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; homer &lt;span class="nt"&gt;-p&lt;/span&gt; homer_data &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT COUNT(*) FROM hep_proto_1;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this fails, the schema was not imported. Re-run the SQL import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; homer &lt;span class="nt"&gt;-p&lt;/span&gt; homer_data &amp;lt; sql/homer_data.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also verify that packets are being inserted. Check homer-capture logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status homer-capture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SIP REGISTER packets missing
&lt;/h3&gt;

&lt;p&gt;Some Asterisk configurations do not send REGISTER through the HEP module. This is normal for outbound-only systems. If you need to capture registrations (common for SIP trunks), enable SIP debugging specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"pjsip set logger on"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But be warned: verbose SIP logging impacts performance on busy systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I monitor calls across a multi-server ViciDial cluster with Homer?
&lt;/h3&gt;

&lt;p&gt;Yes. Configure each Asterisk server to forward HEP to the same Homer instance (set the &lt;code&gt;host&lt;/code&gt; in hep.conf to the shared Homer server). In Homer, search by time range and carrier to correlate calls across the cluster. Each Asterisk server sends a unique &lt;code&gt;capture_id&lt;/code&gt; in its HEP packets, so you can filter by server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Homer capture payload inside SIP messages (e.g., Authorization headers)?
&lt;/h3&gt;

&lt;p&gt;By default, Homer captures full SIP headers including credentials. This is a security risk if calls are being audited by untrusted parties. To mask sensitive data, either disable auth header capture in Homer's config or configure Asterisk to strip headers before HEP forwarding. Consult Homer's privacy settings and your compliance requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the minimum network bandwidth needed for HEP?
&lt;/h3&gt;

&lt;p&gt;A single call with full SIP + RTP + RTCP capture consumes roughly 200-500 KB depending on call duration and RTCP frequency. A 100-call system forwarding to Homer uses 20-50 Mbps of WAN bandwidth. Most carriers throttle SIP signaling to 1-5 Mbps, so HEP overhead is minimal. However, if your Asterisk and Homer servers are on the same LAN with gigabit links, bandwidth is not a concern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run Homer on the same server as Asterisk/ViciDial?
&lt;/h3&gt;

&lt;p&gt;Yes, but not recommended. Homer's MariaDB will consume significant resources during call playback and searches. A dedicated Homer server keeps ViciDial and Asterisk responsive. If you must co-locate, use SSD storage, allocate at least 8 CPU cores to the VM, and set MariaDB's &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; to 50% of available RAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does Homer show a call with one-way audio but ViciDial marked it as CONNECTED?
&lt;/h3&gt;

&lt;p&gt;ViciDial's &lt;code&gt;CONNECTED&lt;/code&gt; status means Asterisk answered the far end (received a 200 OK), not that audio flowed both ways. One-way audio happens after call setup due to network issues (firewall, NAT, codec mismatch, or carrier routing). Homer's RTP analysis reveals the problem. Use this information to file a support ticket with your carrier or review your firewall rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You have installed Homer on a dedicated server, configured Asterisk to forward SIP and RTP packets via HEP, and learned to search Homer's UI for call detail and quality metrics. When a call fails in ViciDial, Homer's packet trace and RTCP statistics show you whether the problem was signaling (wrong SDP, authentication failure), RTP (no packets, high jitter), or network (firewall, latency). From here, export call data to correlate failures with time, carrier, or agent; set up retention policies to manage database growth; and integrate Homer links into your ViciDial admin screens for one-click access. For ongoing monitoring, periodically review Homer's MOS scores and packet loss metrics to detect carrier degradation before customer complaints arrive.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>Integrating AI Voice Agents with VICIdial Remote Agent</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Fri, 16 Oct 2026 03:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/integrating-ai-voice-agents-with-vicidial-remote-agent-4hbg</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/integrating-ai-voice-agents-with-vicidial-remote-agent-4hbg</guid>
      <description>&lt;h1&gt;
  
  
  Integrating AI Voice Agents with VICIdial Remote Agent
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Remote agents hear AI responses cut off mid-sentence, dropped calls fail to transfer to voice bots, and call recordings show no agent audio: fix agent codec mismatch, RTP stream misconfiguration, and VICIDIAL_AGENT permission gaps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;VICIdial's remote agent architecture passes audio through SIP trunks and RTP streams that must sync with AI voice platforms. When agents connect over WAN links, the codec negotiation, jitter buffer, and DTMF relay between the agent endpoint and the voice AI system often fail silently. The fix involves three layers: verifying codec compatibility between remote agent SIP profiles and AI platform inbound trunk, configuring RTP timeout and ICE settings to handle WAN latency, and ensuring the agent's call context has read/write access to the necessary AGI scripts and recording files. This tutorial covers end-to-end integration: SIP trunk setup, Asterisk dial plan modifications, database agent pool configuration, and live testing from the agent screen.&lt;/p&gt;

&lt;p&gt;Tested on VICIdial 2.14 (SVN 3555+), Asterisk 16/18, MariaDB 10.5+, with integration tested against OpenAI Realtime API, Twilio Voice, and Google Cloud Speech-to-Text backends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;VICIdial installation with Asterisk 16 or 18 (kernel 4.19+)&lt;/li&gt;
&lt;li&gt;MariaDB root or vicidial user with GRANT privileges&lt;/li&gt;
&lt;li&gt;Root or sudo access to the VICIdial server&lt;/li&gt;
&lt;li&gt;AI voice platform API credentials and inbound SIP trunk provisioned&lt;/li&gt;
&lt;li&gt;Remote agent machine with soft-phone or SIP device capable of G.711 and G.729 codec support&lt;/li&gt;
&lt;li&gt;Network connectivity test: ping, mtr, and iperf3 between agent and VICIdial server (latency under 150ms ideal)&lt;/li&gt;
&lt;li&gt;Asterisk CLI access via /usr/bin/asterisk -rx&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How remote agent calls reach the AI voice system
&lt;/h2&gt;

&lt;p&gt;In a standard VICIdial setup, an agent logs into the web screen, dials an outbound lead, or receives an inbound call. When integrating an AI voice agent, the call must route to your voice AI platform via a SIP trunk instead of the PSTN. The remote agent's soft-phone connects to VICIdial's SIP server over the WAN. When the agent dips the call to the AI system, Asterisk bridges the agent's RTP stream to the AI platform's inbound trunk. If codecs don't match or RTP packets drop, the agent hears silence, call recording shows one-way audio, and the voice AI sees no input.&lt;/p&gt;

&lt;p&gt;The integration point is the AGI (Asterisk Gateway Interface) script that handles outbound call routing. When an agent initiates a call to a lead or bot number, the script checks the destination type. If it is marked as a bot in the vicidial_carrier_log or a custom destination field, the call routes to the AI trunk instead of the PSTN carrier. Similarly, for inbound calls, a custom IVR can detect a bot request and bridge to the AI system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the AI voice platform SIP trunk
&lt;/h2&gt;

&lt;p&gt;Most AI voice platforms (OpenAI, Twilio, Google) publish SIP endpoint details and authentication. Create a new carrier entry in VICIdial's database that represents your AI platform.&lt;/p&gt;

&lt;p&gt;Log into the VICIdial admin screen and navigate to "Carriers" (or directly edit the database):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;vicidial_carrier_log&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbound_cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;VALUES&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AIVoicePlatform'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sip.openai.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5060&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SIP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'vicidial_user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'secure_password_here'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'N'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'OpenAI Realtime API trunk'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;carrier_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier_host&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_carrier_log&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;carrier_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AIVoicePlatform'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring Asterisk SIP peers for the AI platform
&lt;/h2&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/asterisk/sip-vicidial.conf&lt;/code&gt; or create a separate file included at the bottom of sip.conf. Add an outbound peer definition for the AI platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;AIVoiceOutbound&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="n"&gt;sip&lt;/span&gt;.&lt;span class="n"&gt;openai&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;port&lt;/span&gt;=&lt;span class="m"&gt;5060&lt;/span&gt;
&lt;span class="n"&gt;username&lt;/span&gt;=&lt;span class="n"&gt;vicidial_user&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt;=&lt;span class="n"&gt;secure_password_here&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;=&lt;span class="n"&gt;from&lt;/span&gt;-&lt;span class="n"&gt;internal&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;gsm&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;qualify&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;qualify_timeout&lt;/span&gt;=&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;qualify_freq&lt;/span&gt;=&lt;span class="m"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;directmedia&lt;/span&gt;=&lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;trustrpid&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;sendrpid&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;insecure&lt;/span&gt;=&lt;span class="n"&gt;port&lt;/span&gt;,&lt;span class="n"&gt;invite&lt;/span&gt;
&lt;span class="n"&gt;fromuser&lt;/span&gt;=&lt;span class="n"&gt;vicidial_pbx&lt;/span&gt;
&lt;span class="n"&gt;fromdomain&lt;/span&gt;=&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;dtmfmode&lt;/span&gt;=&lt;span class="n"&gt;rfc2833&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For inbound calls from the AI platform, add a user definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;AIVoiceInbound&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;user&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;=&lt;span class="n"&gt;from&lt;/span&gt;-&lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;voice&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;gsm&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;dtmfmode&lt;/span&gt;=&lt;span class="n"&gt;rfc2833&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the SIP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the peer is registered or reachable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show peers"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for AIVoiceOutbound in the list with status "OK" (for reachable) or "Unmonitored" (for no qualify).&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a dial plan for AI voice routing
&lt;/h2&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/asterisk/extensions-vicidial.conf&lt;/code&gt; and add a context to handle calls destined for the AI voice platform. This context should be called when an agent initiates a call to a "bot" number or when the system detects a bot flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;from&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;-&lt;span class="n"&gt;to&lt;/span&gt;-&lt;span class="n"&gt;ai&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Inbound&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;AI&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;: ${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;language&lt;/span&gt;)=&lt;span class="n"&gt;en&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CDR&lt;/span&gt;(&lt;span class="n"&gt;disposition&lt;/span&gt;)=&lt;span class="n"&gt;TRANSFERRED_TO_AI&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CDR&lt;/span&gt;(&lt;span class="n"&gt;cid_name&lt;/span&gt;)=&lt;span class="n"&gt;Agent_&lt;/span&gt;${&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;num&lt;/span&gt;)})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;name&lt;/span&gt;)=&amp;lt;&lt;span class="n"&gt;Agent&lt;/span&gt; ${&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;num&lt;/span&gt;)}&amp;gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;}@&lt;span class="n"&gt;AIVoiceOutbound&lt;/span&gt;,&lt;span class="m"&gt;45&lt;/span&gt;,&lt;span class="n"&gt;tT&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Log&lt;/span&gt;(&lt;span class="n"&gt;NOTICE&lt;/span&gt;,&lt;span class="n"&gt;Call&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;AI&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;: ${&lt;span class="n"&gt;DIALSTATUS&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()

[&lt;span class="n"&gt;from&lt;/span&gt;-&lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;voice&lt;/span&gt;]
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="n"&gt;NoOp&lt;/span&gt;(&lt;span class="n"&gt;Inbound&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;AI&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;: ${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CDR&lt;/span&gt;(&lt;span class="n"&gt;cid_name&lt;/span&gt;)=&lt;span class="n"&gt;AI_RESPONSE&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;CHANNEL&lt;/span&gt;(&lt;span class="n"&gt;language&lt;/span&gt;)=&lt;span class="n"&gt;en&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Macro&lt;/span&gt;(&lt;span class="n"&gt;agent&lt;/span&gt;-&lt;span class="n"&gt;route&lt;/span&gt;,${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Hangup&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first context routes outbound calls from an agent to the AI platform. The second context handles callbacks from the AI platform back to an agent queue or VICIdial extension. Reload extensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"dialplan reload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Modifying the VICIdial agent dial routine
&lt;/h2&gt;

&lt;p&gt;The core agent dialing logic lives in the AGI scripts. Locate the main agent call handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /usr/share/astguiclient/agi/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VICIdial typically uses an AGI script (often named something like &lt;code&gt;call_agent.agi&lt;/code&gt; or routed via the extensions context &lt;code&gt;from-internal&lt;/code&gt;). The script must detect when a call destination is a bot or AI service, then route to the AI carrier instead of the standard PSTN carrier.&lt;/p&gt;

&lt;p&gt;Create a custom AGI script or modify the existing one. Here is a simplified example that checks the destination in the database and routes accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/perl&lt;/span&gt;
use DBI&lt;span class="p"&gt;;&lt;/span&gt;
use Asterisk::AGI&lt;span class="p"&gt;;&lt;/span&gt;
use strict&lt;span class="p"&gt;;&lt;/span&gt;

my &lt;span class="nv"&gt;$agi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; new Asterisk::AGI&lt;span class="p"&gt;;&lt;/span&gt;
my %hash &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$agi&lt;/span&gt;-&amp;gt;ReadParse&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

my &lt;span class="nv"&gt;$exten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'extension'&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
my &lt;span class="nv"&gt;$channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'channel'&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
my &lt;span class="nv"&gt;$agent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'callerid'&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Database connection (adjust DSN to your setup)&lt;/span&gt;
my &lt;span class="nv"&gt;$dbh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; DBI-&amp;gt;connect&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dbi:mysql:asterisk:localhost'&lt;/span&gt;, &lt;span class="s1"&gt;'vicidial'&lt;/span&gt;, &lt;span class="s1"&gt;'vicidial_password'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Query the destination to see if it is an AI bot&lt;/span&gt;
my &lt;span class="nv"&gt;$sth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$dbh&lt;/span&gt;-&amp;gt;prepare&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"SELECT bot_flag FROM vicidial_agent_pool WHERE agent_id = ? AND bot_flag = 'Y' LIMIT 1"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$sth&lt;/span&gt;-&amp;gt;execute&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$agent_id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
my &lt;span class="nv"&gt;$is_bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sth&lt;/span&gt;-&amp;gt;fetchrow_array&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$sth&lt;/span&gt;-&amp;gt;finish&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Check if the dialed extension matches a bot carrier&lt;/span&gt;
my &lt;span class="nv"&gt;$bot_sth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$dbh&lt;/span&gt;-&amp;gt;prepare&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"SELECT carrier_id FROM vicidial_carrier_log WHERE carrier_name LIKE '%AI%' OR carrier_name LIKE '%Bot%' LIMIT 1"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$bot_sth&lt;/span&gt;-&amp;gt;execute&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
my &lt;span class="nv"&gt;$ai_carrier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$bot_sth&lt;/span&gt;-&amp;gt;fetchrow_array&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$bot_sth&lt;/span&gt;-&amp;gt;finish&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$is_bot&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$exten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ /^555/&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  &lt;span class="c"&gt;# Example: 555xxxx is reserved for bots&lt;/span&gt;
    &lt;span class="nv"&gt;$agi&lt;/span&gt;-&amp;gt;exec&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Dial'&lt;/span&gt;, &lt;span class="s2"&gt;"SIP/&lt;/span&gt;&lt;span class="nv"&gt;$exten&lt;/span&gt;&lt;span class="se"&gt;\@&lt;/span&gt;&lt;span class="s2"&gt;AIVoiceOutbound,30,tT"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# Standard PSTN routing&lt;/span&gt;
    &lt;span class="nv"&gt;$agi&lt;/span&gt;-&amp;gt;exec&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Dial'&lt;/span&gt;, &lt;span class="s2"&gt;"SIP/&lt;/span&gt;&lt;span class="nv"&gt;$exten&lt;/span&gt;&lt;span class="se"&gt;\@&lt;/span&gt;&lt;span class="s2"&gt;StandardCarrier,30,tT"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$dbh&lt;/span&gt;-&amp;gt;disconnect&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is illustrative. In production, integrate this logic into your existing AGI handler or use VICIdial's built-in call routing tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring remote agent soft-phone settings
&lt;/h2&gt;

&lt;p&gt;Remote agents connect to VICIdial via SIP. The agent's soft-phone must be configured with the VICIdial SIP server credentials. Common soft-phones include Zoiper, Linphone, or MicroSIP.&lt;/p&gt;

&lt;p&gt;Example SIP configuration for a remote agent soft-phone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SIP Server: 192.168.1.100 (your VICIdial server)&lt;/li&gt;
&lt;li&gt;Port: 5060&lt;/li&gt;
&lt;li&gt;Username: agent_001&lt;/li&gt;
&lt;li&gt;Password: agent_password&lt;/li&gt;
&lt;li&gt;Display Name: Agent 001&lt;/li&gt;
&lt;li&gt;Codecs (in order): G.711u (ulaw), G.729, GSM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test the connection by placing a call from the agent soft-phone to an extension on the VICIdial system. In the Asterisk CLI, confirm the channel appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"sip show channels"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a channel like &lt;code&gt;SIP/agent_001-00000123&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ensuring codec compatibility between agent and AI platform
&lt;/h2&gt;

&lt;p&gt;Both the agent's SIP connection and the AI platform trunk must support at least one common codec. G.711u (ulaw) is the most widely supported but has higher bandwidth. G.729 requires a license but compresses better. GSM is lightweight but lower quality.&lt;/p&gt;

&lt;p&gt;Check what codecs the AI platform accepts. Most document this in their SIP trunk setup guide. In your sip-vicidial.conf for the AIVoiceOutbound peer, list codecs in order of preference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;gsm&lt;/span&gt;,&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, ensure the agent soft-phone supports these codecs. If a mismatch occurs, Asterisk will attempt transcoding on the fly, which increases CPU load and introduces latency. Force a common codec in the dial plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;}@&lt;span class="n"&gt;AIVoiceOutbound&lt;/span&gt;/&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="m"&gt;45&lt;/span&gt;,&lt;span class="n"&gt;tT&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor codec selection on active calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show channels verbose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for lines like &lt;code&gt;Format: ulaw (ulaw)&lt;/code&gt; or &lt;code&gt;Format: unknown&lt;/code&gt; (which indicates a codec issue).&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling RTP and jitter in WAN environments
&lt;/h2&gt;

&lt;p&gt;Remote agents connecting over public internet face variable latency and packet loss. VICIdial's Asterisk must be configured to handle this gracefully.&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/asterisk/rtp.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;general&lt;/span&gt;]
&lt;span class="n"&gt;rtpstart&lt;/span&gt;=&lt;span class="m"&gt;10000&lt;/span&gt;
&lt;span class="n"&gt;rtpend&lt;/span&gt;=&lt;span class="m"&gt;20000&lt;/span&gt;
&lt;span class="n"&gt;rtcpcheck&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;icesupport&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;dtlscerti&lt;/span&gt;=/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;keys&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;.&lt;span class="n"&gt;crt&lt;/span&gt;
&lt;span class="n"&gt;dtlscertfile&lt;/span&gt;=/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;keys&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;.&lt;span class="n"&gt;key&lt;/span&gt;
&lt;span class="n"&gt;dtlssetup&lt;/span&gt;=&lt;span class="n"&gt;actpass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ICE (Interactive Connectivity Establishment) support helps agents behind NAT/firewalls find the best path. DTLS adds encryption and works with SRTP (Secure RTP).&lt;/p&gt;

&lt;p&gt;Increase the jitter buffer size for WAN calls. Edit &lt;code&gt;/etc/asterisk/asterisk.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;options&lt;/span&gt;]
&lt;span class="n"&gt;maxfiles&lt;/span&gt;=&lt;span class="m"&gt;131072&lt;/span&gt;
&lt;span class="n"&gt;debug&lt;/span&gt;=&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the SIP peer for remote agents, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;RemoteAgent&lt;/span&gt;]
&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="n"&gt;peer&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;=&lt;span class="n"&gt;dynamic&lt;/span&gt;
&lt;span class="n"&gt;allow&lt;/span&gt;=&lt;span class="n"&gt;ulaw&lt;/span&gt;,&lt;span class="n"&gt;alaw&lt;/span&gt;,&lt;span class="n"&gt;gsm&lt;/span&gt;,&lt;span class="n"&gt;g729&lt;/span&gt;
&lt;span class="n"&gt;disallow&lt;/span&gt;=&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="n"&gt;nat&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;qualify&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;qualify_timeout&lt;/span&gt;=&lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;qualify_freq&lt;/span&gt;=&lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;icesupport&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;directmedia&lt;/span&gt;=&lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;jbenable&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;span class="n"&gt;jbimpl&lt;/span&gt;=&lt;span class="n"&gt;adaptive&lt;/span&gt;
&lt;span class="n"&gt;jbmaxsize&lt;/span&gt;=&lt;span class="m"&gt;200&lt;/span&gt;
&lt;span class="n"&gt;useclientcode&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jitter buffer (jbenable=yes, jbimpl=adaptive) smooths out packet arrival timing. jbmaxsize=200 allows up to 200ms of buffering.&lt;/p&gt;

&lt;p&gt;Test latency from the agent machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &lt;span class="nt"&gt;-c&lt;/span&gt; 10 192.168.1.100
mtr &lt;span class="nt"&gt;-c&lt;/span&gt; 20 192.168.1.100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Latency should be under 100ms; above 150ms, VoIP quality degrades noticeably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing and retrieving agent call context in the database
&lt;/h2&gt;

&lt;p&gt;When an agent initiates a call to an AI bot, context about the agent (ID, name, campaign) and the lead (phone, notes) must be passed to the AI system. This is often done via SIP headers or call recording metadata.&lt;/p&gt;

&lt;p&gt;VICIdial stores agent login and call state in the &lt;code&gt;vicidial_agent_log&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;campaign_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_agent_log&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'agent_001'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before routing a call to the AI platform, query the agent's current campaign and pass it as a custom SIP header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;SIP_HEADERS&lt;/span&gt;(&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Agent&lt;/span&gt;-&lt;span class="n"&gt;Campaign&lt;/span&gt;)=${&lt;span class="n"&gt;CAMPAIGN&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;SIP_HEADERS&lt;/span&gt;(&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Lead&lt;/span&gt;-&lt;span class="n"&gt;Phone&lt;/span&gt;)=${&lt;span class="n"&gt;EXTEN&lt;/span&gt;})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Set&lt;/span&gt;(&lt;span class="n"&gt;SIP_HEADERS&lt;/span&gt;(&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Agent&lt;/span&gt;-&lt;span class="n"&gt;ID&lt;/span&gt;)=${&lt;span class="n"&gt;CALLERID&lt;/span&gt;(&lt;span class="n"&gt;num&lt;/span&gt;)})
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;}@&lt;span class="n"&gt;AIVoiceOutbound&lt;/span&gt;,&lt;span class="m"&gt;45&lt;/span&gt;,&lt;span class="n"&gt;tT&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the AI platform side (if you control both endpoints), parse these headers to load agent context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording calls between agent and AI voice platform
&lt;/h2&gt;

&lt;p&gt;By default, VICIdial records all calls to the recordings directory (typically &lt;code&gt;/var/spool/asterisk/monitor/&lt;/code&gt;). When a call is transferred to an AI platform, Asterisk bridges the channels, and the recording continues. However, if the bridge is not set to "mix audio," only one direction is captured.&lt;/p&gt;

&lt;p&gt;Ensure calls are recorded in mixed mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;MixMonitor&lt;/span&gt;(/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;spool&lt;/span&gt;/&lt;span class="n"&gt;asterisk&lt;/span&gt;/&lt;span class="n"&gt;monitor&lt;/span&gt;/${&lt;span class="n"&gt;UNIQUEID&lt;/span&gt;}.&lt;span class="n"&gt;wav&lt;/span&gt;,&lt;span class="n"&gt;ab&lt;/span&gt;)
&lt;span class="n"&gt;exten&lt;/span&gt; =&amp;gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;.,&lt;span class="n"&gt;n&lt;/span&gt;,&lt;span class="n"&gt;Dial&lt;/span&gt;(&lt;span class="n"&gt;SIP&lt;/span&gt;/${&lt;span class="n"&gt;EXTEN&lt;/span&gt;}@&lt;span class="n"&gt;AIVoiceOutbound&lt;/span&gt;,&lt;span class="m"&gt;45&lt;/span&gt;,&lt;span class="n"&gt;tT&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ab&lt;/code&gt; flags mean: a = record agent side, b = record the other side.&lt;/p&gt;

&lt;p&gt;After the call, the file &lt;code&gt;/var/spool/asterisk/monitor/${UNIQUEID}.wav&lt;/code&gt; will contain both sides of the conversation. VICIdial's database log in &lt;code&gt;vicidial_log&lt;/code&gt; or &lt;code&gt;vicidial_closer_log&lt;/code&gt; will record the filename in the &lt;code&gt;recording_id&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;Query call recordings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recording_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;agent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'agent_001'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listen to the recording at &lt;code&gt;/var/spool/asterisk/monitor/recording_id.wav&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the integration end-to-end
&lt;/h2&gt;

&lt;p&gt;Set up a test agent account and soft-phone. Log the agent into the VICIdial web screen at &lt;code&gt;/agc/vicidial.php&lt;/code&gt;. Place a test call to a number that maps to your AI platform (either via a bot flag in the database or a special 555xxxx extension).&lt;/p&gt;

&lt;p&gt;On the server, tail the Asterisk log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/asterisk/messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor call state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"core show channels verbose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the call, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both agent and AI platform RTP streams are active (no "No RTP activity" warnings).&lt;/li&gt;
&lt;li&gt;Codec negotiation completes (look for "Format: ulaw" or similar).&lt;/li&gt;
&lt;li&gt;Call recording file is created and contains audio from both sides.&lt;/li&gt;
&lt;li&gt;After hangup, call record appears in vicidial_log with correct disposition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example successful log output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2024-01-15 14:23:45] NOTICE[1234][C-0000a1b2]: app_dial.c:xxxx: Called SIP/agent_001@RemoteAgent
[2024-01-15 14:23:47] NOTICE[1234][C-0000a1b2]: app_dial.c:xxxx: SIP/agent_001@RemoteAgent is ringing
[2024-01-15 14:23:50] NOTICE[1234][C-0000a1b2]: app_dial.c:xxxx: SIP/agent_001@RemoteAgent answered call
[2024-01-15 14:23:50] NOTICE[1234][C-0000a1b2]: bridge.c:xxxx: Bridging SIP/agent_001@RemoteAgent with SIP/5551234567@AIVoiceOutbound
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the call fails to bridge, check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"No route to host" or timeout: Network/firewall issue between VICIdial and AI platform.&lt;/li&gt;
&lt;li&gt;"Codec not compatible": Add a codec translation or force a common one.&lt;/li&gt;
&lt;li&gt;"Authentication failed": Verify carrier username and password in sip-vicidial.conf.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Agent hears silence or one-way audio during AI voice calls&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check codec negotiation by running &lt;code&gt;asterisk -rx "sip show channels"&lt;/code&gt; during an active call. If the format shown is "unknown" or mismatched between channels, add explicit codec forcing to the dial plan. Verify that the AI platform trunk supports the codec your agent's soft-phone negotiates. Run a SIP debug capture: &lt;code&gt;asterisk -rx "sip set debug on"&lt;/code&gt; and review SDP payloads in the logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calls disconnect after 30 seconds&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is often a SIP timeout or lack of RTP activity. Edit the Dial() command to increase the timeout: &lt;code&gt;Dial(SIP/${EXTEN}@AIVoiceOutbound,120,tT)&lt;/code&gt; instead of the default 30 seconds. If RTP is not flowing, check that directmedia=no is set on the peer and that the agent and platform are using compatible RTP ports (rtpstart/rtpend in rtp.conf should not overlap with other services).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent's soft-phone does not register to VICIdial&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ensure the agent's SIP username and password match an entry in the VICIdial database (typically in a custom user table or in Asterisk's AstDB). Verify the agent soft-phone is pointing to the correct VICIdial server IP and port 5060. Check firewall rules on the VICIdial server: &lt;code&gt;sudo ufw allow 5060/udp&lt;/code&gt; or disable iptables temporarily to isolate the issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recordings are one-sided or empty&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ensure MixMonitor is called before Dial() in the extensions context. Check that &lt;code&gt;/var/spool/asterisk/monitor/&lt;/code&gt; has write permissions for the asterisk user: &lt;code&gt;ls -la /var/spool/asterisk/monitor/&lt;/code&gt;. If the directory doesn't exist, create it: &lt;code&gt;sudo mkdir -p /var/spool/asterisk/monitor &amp;amp;&amp;amp; sudo chown asterisk:asterisk /var/spool/asterisk/monitor/&lt;/code&gt;. Verify that app_mixmonitor is loaded: &lt;code&gt;asterisk -rx "core show modules" | grep mixmonitor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High latency or jitter causing audio drops&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run a continuous ping and mtr test to the AI platform endpoint to measure latency. If consistent latency is above 150ms, check network routing via traceroute. On the VICIdial side, increase the jitter buffer: set jbmaxsize=400 and jbimpl=adaptive in the SIP peer config. Reduce audio codec bandwidth by forcing G.729 (if licensed) or GSM instead of G.711u.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI platform returns authentication failure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Double-check the carrier_user and carrier_pass in the vicidial_carrier_log table. Many AI platforms issue SIP credentials that include a domain or realm; include those in the username field if necessary (e.g., &lt;code&gt;vicidial_user@platform.example.com&lt;/code&gt;). Test the credentials independently with a SIP client like Linphone to confirm they work before debugging the Asterisk integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbound calls from AI platform do not route to agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verify the [from-ai-voice] context exists in extensions-vicidial.conf and includes a macro or dialplan to route to your agent queue. Ensure the AI platform is configured to send inbound calls to the correct VICIdial server IP and extension. Check that the VICIdial firewall allows inbound SIP on port 5060 from the AI platform's public IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use G.729 codec to save bandwidth for remote agents?
&lt;/h3&gt;

&lt;p&gt;Yes, but you must have a G.729 codec license installed on Asterisk. Purchase licenses from DIGIUM or a reseller and install them in /usr/lib/asterisk/modules/. After installation, add &lt;code&gt;allow=g729&lt;/code&gt; to the relevant SIP peer configs. Confirm the codec loads by running &lt;code&gt;asterisk -rx "core show modules" | grep g729&lt;/code&gt;. Note that transcoding between G.729 and the AI platform's codec (often G.711) increases CPU load; if you have many simultaneous calls, stick with G.711u unless bandwidth is severely constrained.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I pass agent custom data or notes to the AI voice bot?
&lt;/h3&gt;

&lt;p&gt;Store the data in the VICIdial lead or agent record, then retrieve it in the AGI script before dialing. Query the vicidial_list table for lead notes or the vicidial_agent_log table for agent notes. Pass the data as SIP headers (e.g., &lt;code&gt;SIP_HEADERS(X-Lead-Notes)=${NOTES}&lt;/code&gt;) or embed it in the dialed extension (e.g., &lt;code&gt;SIP/555${LEAD_ID}@AIVoiceOutbound&lt;/code&gt;). The AI platform must be configured to parse these headers or extension patterns on the inbound side.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the AI platform uses WebSocket instead of SIP?
&lt;/h3&gt;

&lt;p&gt;VICIdial and Asterisk 16/18 do not have native WebSocket support for calls. However, you can bridge a SIP call to a WebSocket endpoint using a gateway service like Jambonz or FreeSWITCH in between. Alternatively, use the AI platform's SIP trunk if available (most offer one). If WebSocket is your only option, consider deploying a dedicated SIP-to-WebSocket gateway on the same network and pointing the VICIdial trunk to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I monitor call quality metrics like MOS score during AI agent calls?
&lt;/h3&gt;

&lt;p&gt;Asterisk does not natively compute MOS (Mean Opinion Score). However, you can enable RTP statistics logging and use third-party tools like SIPp or QEMU to analyze packet loss and jitter. For production monitoring, integrate with a service like Twilio or Vonage that provides call quality dashboards, or deploy an open-source RTPFLOW analysis tool. Store CDR data in VICIdial's database and correlate it with Asterisk logs to identify patterns in call failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I failover to a second AI voice platform if the primary is unreachable?
&lt;/h3&gt;

&lt;p&gt;Yes. In the dial plan, use Dial()'s multiple target syntax to try the primary platform first, then fall back to a secondary. For example: &lt;code&gt;Dial(SIP/${EXTEN}@AIVoicePrimary,20,tT|SIP/${EXTEN}@AIVoiceSecondary,20,tT)&lt;/code&gt;. If the primary times out or is unreachable, Asterisk will attempt the secondary. Ensure both platforms are defined as separate peers in sip-vicidial.conf. Monitor each platform's uptime and update DNS or config entries if you swap primary and secondary roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Integration of AI voice agents with VICIdial remote agents requires correct SIP trunk provisioning for the AI platform, codec alignment between agent soft-phones and the voice platform, and RTP/jitter buffer tuning for WAN stability. Implement custom AGI logic to route calls from agents to the AI carrier based on bot flags or reserved extension ranges. Configure remote agent soft-phones with compatible codecs and NAT settings, enforce MixMonitor for call recording on both sides of the bridge, and test end-to-end with a real agent and soft-phone before deploying to production.&lt;/p&gt;

&lt;p&gt;Monitor SIP show peers to confirm trunk registration, verify call recordings contain both agent and AI audio, and check vicidial_log for call disposition after each test. Common failure points are codec mismatch (fix via allow= directives), one-sided audio (enable MixMonitor with ab flags), and connectivity issues (test mtr and firewall rules). Once a test call succeeds with full audio, appropriate codec negotiation, and recorded wav file, the integration is ready for live campaigns.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>How to Manage ViciDial Lists: Deactivate, Reset &amp; Delete Leads</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Thu, 15 Oct 2026 03:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/how-to-manage-vicidial-lists-deactivate-reset-delete-leads-heo</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/how-to-manage-vicidial-lists-deactivate-reset-delete-leads-heo</guid>
      <description>&lt;h1&gt;
  
  
  How to Manage ViciDial Lists: Deactivate, Reset &amp;amp; Delete Leads
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Remove, reset, or purge unwanted contacts from ViciDial lists without corrupting call logs or orphaning agent records.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;List management in ViciDial requires direct database access combined with understanding how leads link to call logs, agent activity, and callbacks. Deactivating a lead preserves history; resetting a lead clears its attempt count and status; deleting a lead removes it entirely but risks breaking referential integrity if done carelessly. This guide shows the exact SQL, CLI commands, and web interface steps to manage each operation safely.&lt;/p&gt;

&lt;p&gt;Tested on ViciDial 2.14 (SVN 3555+) and 2.13, Asterisk 16/18, MariaDB 10.5+, PHP 7.4+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SSH access to the ViciDial server or a machine with remote MySQL client installed.&lt;/li&gt;
&lt;li&gt;MySQL or MariaDB command-line client (mysql) or a GUI tool like DBeaver or phpMyAdmin.&lt;/li&gt;
&lt;li&gt;Database credentials with SELECT, UPDATE, DELETE on the asterisk database.&lt;/li&gt;
&lt;li&gt;Understanding of ViciDial list IDs (numeric identifiers for campaigns/lists).&lt;/li&gt;
&lt;li&gt;A backup of the asterisk database taken within the last 24 hours.&lt;/li&gt;
&lt;li&gt;Agent or admin access to the web interface at /vicidial/admin.php.&lt;/li&gt;
&lt;li&gt;Knowledge of which leads are tied to active callbacks or scheduled follow-ups.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding ViciDial lead status codes
&lt;/h2&gt;

&lt;p&gt;Before modifying leads, know what each status means in the vicidial_list table.&lt;/p&gt;

&lt;p&gt;The status field controls whether an agent can dial a lead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NEW&lt;/strong&gt;: Untouched, ready to dial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACTIVE&lt;/strong&gt;: Currently being worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEAD&lt;/strong&gt;: Permanently removed from dialing queue (do not call, duplicate, bad number).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INBOUND&lt;/strong&gt;: Reserved for inbound transfers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QUEUE&lt;/strong&gt;: Waiting for agent availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PAUSE&lt;/strong&gt;: Temporarily held.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Related flags in the same table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Called&lt;/strong&gt;: 0 = never called, 1 = called at least once. Resetting this is what "resetting" a lead means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone&lt;/strong&gt;: The phone number string (E.164 format preferred: +1-555-0101).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead_id&lt;/strong&gt;: Auto-incremented primary key, unique per list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List_id&lt;/strong&gt;: Foreign key linking to vicidial_lists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The vicidial_log table records every call attempt. Deleting a lead does not automatically delete its logs, so historical call data remains for reporting and compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deactivating leads through the web interface
&lt;/h2&gt;

&lt;p&gt;The simplest method for small volumes (10-100 leads) is the ViciDial admin panel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to Admin Leads (e.g., &lt;a href="http://your-vicidial-server/vicidial/admin.php" rel="noopener noreferrer"&gt;http://your-vicidial-server/vicidial/admin.php&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Select your List from the dropdown.&lt;/li&gt;
&lt;li&gt;Use the Search form to find the lead by phone, name, or ID.&lt;/li&gt;
&lt;li&gt;Click the lead row to open its detail page.&lt;/li&gt;
&lt;li&gt;Change Status to DEAD.&lt;/li&gt;
&lt;li&gt;Set "Do Not Call" flag if you want to block it from future list reloads.&lt;/li&gt;
&lt;li&gt;Save.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This marks the lead as DEAD in vicidial_list.status, preventing agents from dialing it. The lead remains in the database for audit purposes. Call logs stay intact.&lt;/p&gt;

&lt;p&gt;Use this method when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are removing a single lead or a handful.&lt;/li&gt;
&lt;li&gt;You need a timestamp audit trail (ViciDial logs admin actions).&lt;/li&gt;
&lt;li&gt;You want agent feedback recorded (notes field).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For bulk operations, use the database directly (see below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Deactivating leads in bulk with SQL
&lt;/h2&gt;

&lt;p&gt;To deactivate 100+ leads at once, use direct SQL. This is 100 times faster than the web interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Identify the list ID and lead range.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_lists&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Campaign_Q4%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the list_id (e.g., 101).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Deactivate leads by phone number pattern.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'555%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This marks all leads in list 101 with phone numbers starting in 555 as DEAD. The called = 1 ensures they appear in reporting as "attempted".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Verify the change.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; 
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;count(*)  status
-------  -------
  4521   NEW
   512   DEAD
   189   PAUSE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common deactivation scenarios:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deactivate all leads in a list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deactivate leads with invalid phone numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deactivate leads marked as duplicate in notes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%duplicate%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deactivate leads not called in 30 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;last_local_call_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After any bulk update, always run the verification query to confirm the count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resetting leads (clearing attempts and status)
&lt;/h2&gt;

&lt;p&gt;Resetting a lead removes its call history and attempt counter, returning it to NEW status. This re-queues it for dialing without losing the contact record itself.&lt;/p&gt;

&lt;p&gt;Use reset when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An agent dialed a number by mistake (robocall or wrong extension).&lt;/li&gt;
&lt;li&gt;A lead was marked DEAD but is actually reachable (bad skip trace).&lt;/li&gt;
&lt;li&gt;You want to retry a lead that failed several times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reset a single lead by ID:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NEW'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4521&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reset all leads in a list that are currently PAUSE or DEAD:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NEW'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PAUSE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DEAD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reset leads that have not been called in the last 60 days:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NEW'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_call_time&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;last_call_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not reset the entire active_in_list or priority fields; those control dialing order and should remain intact.&lt;/p&gt;

&lt;p&gt;Resetting does not delete call logs. The lead's history in vicidial_log remains for compliance. You are only clearing the queuing state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting leads permanently
&lt;/h2&gt;

&lt;p&gt;Permanent deletion removes a lead entirely from the vicidial_list table. Use this only when you are certain the lead should never appear in reports or callbacks.&lt;/p&gt;

&lt;p&gt;Risks of deleting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Orphaned call logs: A vicidial_log record pointing to a deleted lead_id cannot be joined back.&lt;/li&gt;
&lt;li&gt;Broken callbacks: If a callback is scheduled for a deleted lead_id, it will fail.&lt;/li&gt;
&lt;li&gt;Compliance issues: You lose the record that a contact was in your system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For these reasons, deactivation (DEAD status) is safer than deletion in most cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete a single lead (verify first):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check for callbacks&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;callback_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_date&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_callbacks&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4521&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- If none, delete&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4521&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete leads that were never called:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NEW'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete duplicate phone numbers (keep the first occurrence):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; 
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before running any DELETE, always take a backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqldump &lt;span class="nt"&gt;-u&lt;/span&gt; asterisk &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk vicidial_list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/vicidial_list_backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the DELETE in a transaction so you can roll back if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NEW'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Check affected rows&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ROW_COUNT&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;-- If correct, commit&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- If wrong, ROLLBACK;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Purging call logs for deleted leads
&lt;/h2&gt;

&lt;p&gt;If you delete leads, their call logs remain in vicidial_log. To clean them up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes any orphaned log entries. Run this once per month if you delete leads regularly.&lt;/p&gt;

&lt;p&gt;Check how many rows will be affected first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a large system (millions of logs), this query can take minutes. Use it during low-traffic windows (2-4 AM).&lt;/p&gt;

&lt;h2&gt;
  
  
  Resetting call attempt counters
&lt;/h2&gt;

&lt;p&gt;ViciDial tracks call attempts per lead in the vicidial_log table. Some lists use custom attempt limits (e.g., dial 5 times, then move to DEAD). If you want to allow more attempts without fully resetting the lead, clear the attempt count.&lt;/p&gt;

&lt;p&gt;The vicidial_log.lead_id and vicidial_log.list_id combination forms the basis for attempt counting. To allow a lead to be dialed again without losing history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt; 
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;call_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This resets the called flag for leads called in the last 7 days, letting them dial again. The call logs remain for reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk importing and list resets
&lt;/h2&gt;

&lt;p&gt;When you import a new lead list via the web interface (Admin Leads, Upload), ViciDial assigns new lead_ids and calculates the next_state. If you want to reload the same list (e.g., after adding new fields), you must first reset or delete the old list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To reload a list:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Note the list_id.&lt;/li&gt;
&lt;li&gt;Delete all leads in that list: &lt;code&gt;DELETE FROM vicidial_list WHERE list_id = 101;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Re-import the list via the web interface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Alternatively, use the command-line script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/share/astguiclient/ADMIN_manual_list_reset.pl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--list-id&lt;/span&gt; 101 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--campaign&lt;/span&gt; Outbound_Q4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script safely resets the list, clearing all leads and resetting attempt counts. Check the script logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/astguiclient/ADMIN_manual_list_reset.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using the CLI for list diagnostics
&lt;/h2&gt;

&lt;p&gt;Query the ViciDial database from the command line without entering MySQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; asterisk &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT lead_id, phone, status, called FROM vicidial_list WHERE list_id = 101 LIMIT 10;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get a count by status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; asterisk &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT status, count(*) as count FROM vicidial_list WHERE list_id = 101 GROUP BY status;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find leads with callback scheduled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; asterisk &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT lead_id, callback_date, callback_notes FROM vicidial_callbacks &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   WHERE callback_date &amp;gt; NOW() ORDER BY callback_date LIMIT 20;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check for active agents on a list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; asterisk &lt;span class="nt"&gt;-p&lt;/span&gt; asterisk &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT user, status, lead_id FROM vicidial_agent_status &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   WHERE campaign = 'Outbound_Q4' AND status != 'OFFLINE';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect to the Asterisk console to see if agents are paused or active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"vicidial show agents"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hangup an agent's call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asterisk &lt;span class="nt"&gt;-rx&lt;/span&gt; &lt;span class="s2"&gt;"channel request hangup SIP/2001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem: Leads won't queue after resetting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check if the list itself is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;campaign&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_lists&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;list_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If active = 0, the list is paused. Set active = 1 to resume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem: Deleted leads still appear in agent screen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agents may have cached the lead list in browser memory. Have them log out and log back in, or clear the browser cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem: Call logs show lead_id that no longer exists.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is normal if you deleted leads after calls. To prevent orphaned logs in the future, use DEAD status instead of DELETE.&lt;/p&gt;

&lt;p&gt;To see orphaned leads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;vl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_log&lt;/span&gt; &lt;span class="n"&gt;vl&lt;/span&gt; 
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="n"&gt;vl2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;vl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vl2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vl2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem: Bulk deactivation query timed out.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The vicidial_list table may lack indexes on frequently filtered columns. Add an index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;vicidial_list&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_list_status&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then retry the query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem: Web interface is slow after bulk update.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ViciDial caches list state in memory. Restart the astguiclient services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart astguiclient
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait 30 seconds for caches to rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem: Attempting to delete a lead returns foreign key constraint error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A callback, inbound transfer, or agent note references this lead. Find and remove those references first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_callbacks&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4521&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_inbound_anicode&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4521&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vicidial_agent_notes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4521&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete or reassign those rows, then try deleting the lead again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does ViciDial mark calls as DEAD automatically?
&lt;/h3&gt;

&lt;p&gt;ViciDial can be configured to auto-mark a lead as DEAD after a certain number of failed attempts (no answer, wrong number, answering machine). This is set in the campaign's dialer settings. If you want to disable auto-DEAD, go to Admin Campaigns, select your campaign, and set "Max attempts" to a high number (e.g., 999) or use a custom status like PAUSE instead. Leads marked DEAD manually will not be re-queued unless you reset them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I export a list of deactivated leads?
&lt;/h3&gt;

&lt;p&gt;Yes. Use the web interface (Admin Leads, Export) to download the list as CSV, or run a MySQL query and pipe it to a file: &lt;code&gt;mysql -u asterisk -p asterisk -e "SELECT * FROM vicidial_list WHERE list_id = 101 AND status = 'DEAD';" &amp;gt; /tmp/deactivated_leads.txt&lt;/code&gt;. The CSV export includes all fields and can be reimported into another system for compliance records.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens to agent statistics if I delete a lead?
&lt;/h3&gt;

&lt;p&gt;Deleting a lead does not erase the agent's call history or stats. The vicidial_closer_log records agent performance (talk time, dials, disposition). If you delete the lead after the call, the log entry remains but the agent can no longer view the lead's details in the agent screen. For this reason, use DEAD status instead of deletion if you want to preserve agent accountability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I restore a deleted lead from backup?
&lt;/h3&gt;

&lt;p&gt;Yes, but only if you have a database backup taken before the deletion. Restore the backup to a new database, extract the deleted lead row, and insert it into the production database. ViciDial does not have a trash/undelete feature for individual leads. Always backup before bulk delete operations: &lt;code&gt;mysqldump -u asterisk -p asterisk vicidial_list &amp;gt; /tmp/backup.sql&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I reset leads without stopping active dialing?
&lt;/h3&gt;

&lt;p&gt;Use the database directly (SQL UPDATE statements). Web interface resets may lock the list temporarily. When updating with SQL, the change takes effect immediately without stopping agents. However, agents who are already dialing a lead will not see the reset until they finish the call and the system refreshes the lead state (usually within 2-3 seconds).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You can now deactivate, reset, or delete leads in ViciDial using three approaches: the web interface for single leads and auditing, SQL UPDATE for bulk deactivation, and SQL DELETE for permanent removal (use sparingly). Deactivation (DEAD status) is the safest choice in production because it preserves call logs and agent accountability while removing leads from the dialing queue. Resetting clears the attempt counter and status, re-queuing a lead without losing history. Deleting is permanent and risks breaking callbacks and orphaning logs.&lt;/p&gt;

&lt;p&gt;Always back up the database before bulk operations. Use START TRANSACTION and ROLLBACK if a query affects more rows than expected. After any bulk change, verify counts with SELECT queries and test with a single agent before resuming full-scale dialing. Run orphan log cleanup (DELETE from vicidial_log WHERE lead_id NOT IN...) monthly if you delete leads regularly, and add database indexes if bulk updates time out on large lists (100k+ leads).&lt;/p&gt;

&lt;p&gt;Next steps: review the dialer campaign settings to confirm max-attempt rules are aligned with your deactivation policy, set up a monthly backup cron job if you have not already, and test a reset on a single lead during a quiet period to verify behavior matches your expectations.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
    <item>
      <title>Detecting Inbound Dead Air: Answered Calls Where Asterisk Never Accepts Carrier RTP</title>
      <dc:creator>Moisi Trungu</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:30:02 +0000</pubDate>
      <link>https://dev.to/moisi_trungu_31647b7ac300/detecting-inbound-dead-air-answered-calls-where-asterisk-never-accepts-carrier-rtp-5ckn</link>
      <guid>https://dev.to/moisi_trungu_31647b7ac300/detecting-inbound-dead-air-answered-calls-where-asterisk-never-accepts-carrier-rtp-5ckn</guid>
      <description>&lt;h1&gt;
  
  
  Detecting Inbound Dead Air: Answered Calls Where Asterisk Never Accepts Carrier RTP
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Some inbound calls answer normally at the SIP level, but the PBX never accepts a single RTP packet from the carrier. Asterisk already logs the evidence: a Strict RTP "learning" line that never turns into a lock or a source switch. Here's how I turned that into an alert, corroborated it with Homer RTCP, avoided the early-media trap, and used one SIP header to decide where to send the ticket.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tested on: Asterisk 18 (ViciDial build, chan_sip, &lt;code&gt;strictrtp=seqno&lt;/code&gt;, ConfBridge/softmix agent bridges), Loki, Homer with heplify-server on PostgreSQL 16. Check your Asterisk version's &lt;code&gt;res_rtp_asterisk.c&lt;/code&gt; for the exact log wording.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does inbound dead air look like?
&lt;/h2&gt;

&lt;p&gt;The agent answers and hears nothing. SIP looks normal (&lt;code&gt;INVITE&lt;/code&gt;, &lt;code&gt;183&lt;/code&gt;, &lt;code&gt;200 OK&lt;/code&gt;, &lt;code&gt;ACK&lt;/code&gt;, &lt;code&gt;BYE&lt;/code&gt;), the CDR shows a short answered call, and the agent gets blamed. The first one I caught sat in the agent's bridge for about 6 seconds. Asterisk's log showed the carrier's media address set from SDP, then nothing: no packet from that stream was ever accepted.&lt;/p&gt;

&lt;p&gt;Then it stopped being a one-off. One morning, 26 answered inbound calls in 44 minutes had no carrier RTP accepted at the PBX, across three inbound trunks: 26 of the 125 calls on those trunks that reached an agent bridge, about one in five. The previous day's 12 business hours had 1,330 bridged calls on the same trunks and none like this. What this shows is that no carrier RTP was &lt;em&gt;accepted at the PBX&lt;/em&gt;; assigning blame needs packet captures or carrier evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Asterisk log lines show whether RTP arrived?
&lt;/h2&gt;

&lt;p&gt;With strict RTP enabled (&lt;code&gt;strictrtp=yes&lt;/code&gt;, the default, or &lt;code&gt;seqno&lt;/code&gt;), &lt;code&gt;res_rtp_asterisk.c&lt;/code&gt; logs the source-learning state of every RTP stream at verbose level 4. Real lines from my server, with addresses, IDs and dates replaced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[15:33:36] VERBOSE[17871][C-0001a2b3] res_rtp_asterisk.c: 0x7f7f... -- Strict RTP learning after remote address set to: 198.51.100.23:45892
[13:42:40] VERBOSE[3563][C-0001a0f0] res_rtp_asterisk.c: 0x7f7e... -- Strict RTP learning complete - Locking on source address 203.0.113.10:10256
[13:43:01] VERBOSE[6158][C-0001a0f3] res_rtp_asterisk.c: 0x7f7f... -- Strict RTP switching to RTP target address 203.0.113.20:30818 as source
[13:43:01] VERBOSE[6158][C-0001a0f3] res_rtp_asterisk.c: 0x7f7e... -- Strict RTP switching source address to 192.0.2.108:41882
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters is &lt;em&gt;where in the source&lt;/em&gt; each one is printed (Asterisk 18):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"learning after remote address set to"&lt;/strong&gt; comes from &lt;code&gt;ast_rtp_remote_address_set()&lt;/code&gt;. It fires when the far end's address is set from SDP. No packet is involved. Call it the &lt;strong&gt;open&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Locking on source address"&lt;/strong&gt;, &lt;strong&gt;"switching to RTP target address ... as source"&lt;/strong&gt; and &lt;strong&gt;"switching source address to"&lt;/strong&gt; are all printed in &lt;code&gt;ast_rtp_read()&lt;/code&gt;, the incoming-packet path. Each one means a packet arrived and Asterisk accepted that source. Call any of them a &lt;strong&gt;close&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A missing close doesn't mean zero packets hit the NIC. Strict RTP silently drops packets (debug level only) while it qualifies a &lt;em&gt;different&lt;/em&gt; source address, which takes 4 sequential packets by default. So an open with no close means &lt;strong&gt;Asterisk never accepted a stream for that call&lt;/strong&gt;. A real 50-packet-per-second stream bridged for several seconds should get through probation from any address, so an unpaired open is a strong candidate, but only a candidate.&lt;/p&gt;

&lt;p&gt;Also, SDP says where the carrier wants to &lt;em&gt;receive&lt;/em&gt; media, not where it sends from. A different source shows up as "switching source address to ", so the code pairs any close of the same stream within the carrier's ranges. A source outside your map becomes a false candidate that RTCP refutes.&lt;/p&gt;

&lt;p&gt;Prerequisites: &lt;code&gt;messages&lt;/code&gt; must log &lt;code&gt;verbose&lt;/code&gt; at level 4 or higher, and you must match all three close forms. Over about four months my &lt;code&gt;messages&lt;/code&gt; file had 414,530 opens, 369,649 "Locking", 234,563 "switching to RTP target" and 101,985 "switching source address" lines. Match only "Locking" and you'll flag healthy calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I tell an answered inbound call from a ring that went nowhere?
&lt;/h2&gt;

&lt;p&gt;Unanswered calls also leave unpaired opens, since many carriers send nothing before the answer: 16 of 42 in a two-hour window around the incident. So for each unpaired open, read the lines with the same &lt;code&gt;C-&lt;/code&gt; ID and check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bridge evidence.&lt;/strong&gt; In my ViciDial build the customer channel goes into a ConfBridge softmix conference: &lt;code&gt;Channel SIP/&amp;lt;trunk&amp;gt;-xxxx joined 'softmix' base-bridge&lt;/code&gt;. Ring-group overflow via &lt;code&gt;Dial()&lt;/code&gt; logs &lt;code&gt;app_dial.c: SIP/... answered&lt;/code&gt; instead. The first such line is the &lt;strong&gt;join&lt;/strong&gt;; &lt;code&gt;left 'softmix'&lt;/code&gt; is the &lt;strong&gt;leave&lt;/strong&gt;. MeetMe-based ViciDial setups log different lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direction.&lt;/strong&gt; The context must name the inbound trunk's channel and inbound dialplan context. With one peer name for both directions, a silent outbound leg looks identical at the RTP layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DID and caller ID&lt;/strong&gt;, for the alert. &lt;code&gt;MixMonitor&lt;/code&gt; and my dialplan's &lt;code&gt;Phone call from ...&lt;/code&gt; line both run before the &lt;code&gt;200 OK&lt;/code&gt;, so neither proves an answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the first incident, filtered to its &lt;code&gt;C-&lt;/code&gt; ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15:33:36  Strict RTP learning after remote address set to: 198.51.100.23:45892
15:33:36  Executing [process@trunkinbound:10] Verbose("SIP/carrier_in-...", "Phone call from ...")
15:33:40  Executing [...] ConfBridge("SIP/carrier_in-...", "...,vici_agent_bridge,vici_customer_user")
15:33:40  Channel SIP/carrier_in-... joined 'softmix' base-bridge &amp;lt;...&amp;gt;
15:33:46  Channel SIP/carrier_in-... left 'softmix' base-bridge &amp;lt;...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An open, a join, a leave six seconds later, no close.&lt;/p&gt;

&lt;p&gt;This covers calls that &lt;strong&gt;reached a bridge&lt;/strong&gt;. In the incident window one more call never locked: the PBX answered it into the queue and the carrier hung up 7 seconds later, before any agent. If queue dead air matters to you, anchor on the PBX's &lt;code&gt;200 OK&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long should I wait for a close?
&lt;/h2&gt;

&lt;p&gt;Don't measure the hold from the open, which happens when the call &lt;em&gt;arrives&lt;/em&gt;: ring-group overflow calls ring a long time (48 s in a 12-hour sample).&lt;/p&gt;

&lt;p&gt;Anchor on the join instead. From the raw log file over 12 business hours: 1,330 bridged inbound calls on the three trunks, every one closed, median close in the same second as the join, latest 2 s after it. These time the logged transition, not packet arrival, at one-second resolution. During the incident the latest healthy close was 5 s after the join.&lt;/p&gt;

&lt;p&gt;My rule: no close for the call by &lt;strong&gt;join + 20 s&lt;/strong&gt;. That's several times the worst case, and the candidate is ready well under a minute after the agent picks up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the detector code look like?
&lt;/h2&gt;

&lt;p&gt;My watcher reads the logs from Loki, but tailing &lt;code&gt;messages&lt;/code&gt; works the same. This is the core matching logic, simplified from my production script with documentation-range addresses. It runs as shown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="c1"&gt;# One entry per inbound door. The SAME matcher is used for opens and closes.
&lt;/span&gt;&lt;span class="n"&gt;CARRIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;carrier_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;203\.0\.113\.\d+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;198\.51\.100\.\d+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SIP/carrier_a_in_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;carrier_b_nl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192\.0\.2\.190&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SIP/carrier_b_nl-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;carrier_b_de&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192\.0\.2\.163&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SIP/carrier_b_de-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CARRIERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;re&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:%s):\d+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="c1"&gt;# [C-call tag] ... res_rtp_asterisk.c: 0x&amp;lt;RTP instance&amp;gt; -- Strict RTP &amp;lt;event&amp;gt;
&lt;/span&gt;&lt;span class="n"&gt;RE_STRICT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[(C-[0-9a-f]+)\] res_rtp_asterisk\.c: (0x[0-9a-f]+) -- Strict RTP (.*)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RE_OPEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;learning after remote address set to: (\S+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RE_CLOSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;learning complete - Locking on source address (\S+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|switching to RTP target address (\S+) as source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|switching source address to (\S+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;media_carrier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_port&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CARRIERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;re&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_port&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;lines: [(ts_ns, text)] for ONE PBX, complete for the window.
    Returns (opens, closes, multi).
    opens: the FIRST carrier-facing stream of each call (the inbound leg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s).
    closes: {(call_tag, rtp_instance): [(ts_ns, seq)]}, carrier-range only.
    multi: call tags that opened more than one carrier-facing RTP instance
    (e.g. an overflow dialled out through a carrier). Flag those for review.
    seq is the position after a stable sort by time, so same-second lines
    keep their order.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;opens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RE_STRICT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RE_OPEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;media_carrier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;# later re-opens (re-INVITE) and other legs: not covered
&lt;/span&gt;                &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts_ns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trunk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;media_carrier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
                &lt;span class="n"&gt;opens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RE_CLOSE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ip_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# any address in a carrier range, so a learned source change pairs
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;media_carrier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_port&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;multi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multi&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;first_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upto_ns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;First close of the SAME call and SAME RTP instance after the open and
    no later than upto_ns. Another call reusing the port, or another leg of
    this call, can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t pair it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;closes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts_ns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;upto_ns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Strict RTP line carries the call's &lt;code&gt;C-&lt;/code&gt; tag and the RTP instance pointer (&lt;code&gt;0x...&lt;/code&gt;), so closes are keyed by both, not by port. Carriers reuse ports, so a match keyed on port can let a later call's lock "close" a dead one. Keying on the instance stops another leg of the same call from closing it too, for example an overflow dialled back out through a carrier. Calls with more than one carrier-facing instance come back in &lt;code&gt;multi&lt;/code&gt;, so you can review them; my two days of raw logs had none. Only carrier-range closes count, so a ring-group phone's stream doesn't. The input must be complete and in time order; the stable sort keeps same-second lines in file order.&lt;/p&gt;

&lt;p&gt;A Loki query that feeds it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{server="pbx", job="asterisk"} |= "Strict RTP" |~ "203\\.0\\.113\\.\\d+|198\\.51\\.100\\.\\d+|192\\.0\\.2\\.190|192\\.0\\.2\\.163"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LogQL literals use Go escaping, so a regex &lt;code&gt;\.&lt;/code&gt; is written &lt;code&gt;\\.&lt;/code&gt;. &lt;code&gt;query_range&lt;/code&gt; returns newest-first by default and silently stops at &lt;code&gt;limit&lt;/code&gt;, so use &lt;code&gt;direction=forward&lt;/code&gt;, split any window that hits the limit until none does, and merge before parsing. Then check the pipeline against the source: for one 12-hour window, two Loki runs gave 2,121 and 2,016 opens where the raw &lt;code&gt;messages&lt;/code&gt; file had 2,284. All numbers here come from the raw file.&lt;/p&gt;

&lt;p&gt;Each pass keeps as a &lt;strong&gt;candidate&lt;/strong&gt; any bridged inbound call with no close by &lt;code&gt;join + 20 s&lt;/code&gt; plus an ingest margin, and sends it to Homer before posting.&lt;/p&gt;

&lt;p&gt;Opens and closes must use &lt;strong&gt;the same matcher&lt;/strong&gt;: an alias of mine that matched one carrier's two POP ranges for opens but one for closes made every healthy call on the other POP a false candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can Homer RTCP add?
&lt;/h2&gt;

&lt;p&gt;Your own RTCP &lt;em&gt;corroborates&lt;/em&gt; the log signal; it comes from the same reception state, so it isn't independent proof. The carrier's RTCP, where it sends any, is the closest thing to independent evidence short of packet captures at both ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your side.&lt;/strong&gt; In Asterisk 18 a report carries a reception block once the far end's SSRC is recorded (&lt;code&gt;themssrc_valid&lt;/code&gt;), which happens after a packet passes strict RTP and then stays set. RFC 3550 only requires blocks for sources heard since the previous report, so another stack may drop the block between bursts; check yours. heplify stores RTCP as JSON in &lt;code&gt;hep_proto_5_default&lt;/code&gt;. In 128,786 reports around the incident, &lt;code&gt;report_blocks&lt;/code&gt; was a non-empty array or &lt;code&gt;null&lt;/code&gt;, never empty, missing or unparseable, but the SQL below still treats those as unclear rather than "nothing received".&lt;/p&gt;

&lt;p&gt;Here are my side's reports for one dead call, in seconds relative to the &lt;code&gt;200 OK&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T-0.6  {"sender_information":{"packets":250,...},"report_blocks":null}   &amp;lt;- before the answer
T+4.4  {"sender_information":{"packets":491,...},"report_blocks":null}
T+9.4  {"sender_information":{"packets":741,...},"report_blocks":null}
T+14.4 {"sender_information":{"packets":991,...},"report_blocks":null}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We sent 50 packets a second; no far-end SSRC was ever recorded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The carrier's side.&lt;/strong&gt; Under RFC 3550, a participant that has sent RTP since its last report sends a Sender Report; otherwise a Receiver Report. heplify's decoder only fills &lt;code&gt;sender_information&lt;/code&gt; for a Sender Report, so a Receiver Report shows zeros. The stored &lt;code&gt;type&lt;/code&gt; is useless here (on compound packets it's the last sub-packet, 202 on most of my rows), so I test for a non-zero NTP timestamp; in one hour of my data it always coincided with a non-zero packet count. In that dead call every carrier report had a zero NTP timestamp, and a report block for &lt;em&gt;our&lt;/em&gt; stream with the sequence number climbing about 200 every 4 s.&lt;/p&gt;

&lt;p&gt;Over one hour on that carrier's trunks, all 85 calls where our side reported reception got real Sender Reports from the carrier (1,680 of 1,723 carrier reports had a sender timestamp). The dead calls got Receiver Reports only. That corroborates the log signal: the carrier's RTCP stack didn't consider itself a sender at its SBC. Corroboration, not proof, and silent about further upstream. The other carrier's relay sent no RTCP my capture saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is RTCP from before the answer meaningless?
&lt;/h2&gt;

&lt;p&gt;The first report above was emitted &lt;em&gt;before&lt;/em&gt; the &lt;code&gt;200 OK&lt;/code&gt;. My PBX replies with &lt;code&gt;183 Session Progress&lt;/code&gt; with SDP and starts early media (ringback) at once, then sends the final &lt;code&gt;200 OK&lt;/code&gt; on answer. So it emits RTCP during the ring, when a healthy carrier usually isn't sending yet.&lt;/p&gt;

&lt;p&gt;In the hour before the incident, all 148 bridged calls with my RTCP had our &lt;code&gt;183&lt;/code&gt; before the &lt;code&gt;200 OK&lt;/code&gt;. 48 had a report before the answer, and &lt;strong&gt;all 48 pre-answer reports had no receiver block&lt;/strong&gt;. From 2 s after the answer it flipped: 2,420 reports, all with a block, and no healthy call where they were all empty.&lt;/p&gt;

&lt;p&gt;In the incident window a call torn down 3.7 s after the answer had one pre-answer report from my side, with no block. On RTCP alone it looks dead, but the Asterisk log shows it locked at the answer.&lt;/p&gt;

&lt;p&gt;The rule I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refuted:&lt;/strong&gt; any report from your side, at any time, has a non-empty block. Asterisk accepted a packet. Don't post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed:&lt;/strong&gt; at least one report from your side &lt;strong&gt;2 s or more after the &lt;code&gt;200 OK&lt;/code&gt;&lt;/strong&gt;, and none of your reports for the call, read through the end of the call, has a block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown:&lt;/strong&gt; no post-answer report yet (very short calls, capture lag). Re-check next pass, and say so if it never settles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 26 incident calls came back confirmed: 1 to 5 post-answer reports each from my side, read 20 minutes past the answer (so through the end of every call), none with a block, and no Sender Report from either carrier.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I find the right Homer dialog for a log line?
&lt;/h2&gt;

&lt;p&gt;Homer doesn't know the &lt;code&gt;C-&lt;/code&gt; ID, but the carrier's SDP has the media &lt;code&gt;ip:port&lt;/code&gt;. It's one statement, so no result from a previous call can leak in. Run it with &lt;code&gt;psql -v pbx_ip=... -v sig_ips=a,b -v media_ip=... -v media_port=... -v open_ts='YYYY-MM-DD HH:MM:SS+00' -f&lt;/code&gt;. RTCP is joined to the dialog by &lt;code&gt;sid&lt;/code&gt;, which only works if your heplify correlates RTCP to the SIP Call-ID (mine does; check a known call first). My &lt;code&gt;raw&lt;/code&gt; columns are &lt;code&gt;bytea&lt;/code&gt;; if yours are &lt;code&gt;text&lt;/code&gt;, drop the &lt;code&gt;convert_from(...)&lt;/code&gt; wrappers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- One statement, no carried-over variables. Inputs: pbx_ip, sig_ips (comma list),&lt;/span&gt;
&lt;span class="c1"&gt;-- media_ip, media_port, open_ts. Output: one row with a verdict.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="c1"&gt;-- carrier INVITEs whose SDP carries the open's media endpoint&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'(?i)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;x-oid:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s1"&gt;*([^&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s1"&gt;]+)'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;x_oid&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hep_proto_1_call&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'open_ts'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'90 seconds'&lt;/span&gt;
                          &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'open_ts'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'3 seconds'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;protocol_header&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'dstIp'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'pbx_ip'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;protocol_header&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'srcIp'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_to_array&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="s1"&gt;'sig_ips'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'INVITE %'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;c=IN IP4 '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="s1"&gt;'media_ip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;m=audio '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'media_port'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;       &lt;span class="c1"&gt;-- exactly one dialog, or nothing&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_oid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;x_oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;       &lt;span class="c1"&gt;-- first 200 to an INVITE, sent by the PBX&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hep_proto_1_call&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;
                 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'open_ts'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'90 seconds'&lt;/span&gt;
                                       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'open_ts'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'10 minutes'&lt;/span&gt;
                 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;protocol_header&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'srcIp'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'pbx_ip'&lt;/span&gt;
                 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'^SIP/2&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;0 200 '&lt;/span&gt;
                 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~*&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;cseq:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s1"&gt;*[0-9]+&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s1"&gt;+invite'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t200&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;       &lt;span class="c1"&gt;-- every RTCP report of that dialog, classified&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;protocol_header&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'srcIp'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;                                &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'unparsed'&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'report_blocks'&lt;/span&gt;                  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'missing'&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;jsonb_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'report_blocks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'null'&lt;/span&gt;  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;jsonb_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'report_blocks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'array'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'malformed'&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;jsonb_array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'report_blocks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'empty'&lt;/span&gt;
              &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'blocks'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;hep_proto_5_default&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_date&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t200&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'2 minutes'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t200&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'20 minutes'&lt;/span&gt;
  &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
                                  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;convert_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTF8'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t200&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'unparsed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'missing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'malformed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'empty'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;unclear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'pbx_ip'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'blocks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pbx_with_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'pbx_ip'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;create_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t200&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'2 seconds'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pbx_post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'pbx_ip'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;
                     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;create_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t200&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'2 seconds'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pbx_post_none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'pbx_ip'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;jsonb_typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'sender_information'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;
                     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'sender_information'&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'ntp_timestamp_sec'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'^[0-9]+$'&lt;/span&gt;
                     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'sender_information'&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'ntp_timestamp_sec'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;far_end_sr&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dialogs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x_oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t200&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;             &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;
       &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pbx_with_block&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;                    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'refuted'&lt;/span&gt;
       &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unclear&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pbx_post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;
       &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pbx_post_none&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pbx_post&lt;/span&gt;            &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;
       &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The INVITE window allows 3 s of clock skew after the open. The verdict is &lt;code&gt;unknown&lt;/code&gt; unless exactly one dialog matched and it has a PBX &lt;code&gt;200&lt;/code&gt; whose &lt;code&gt;CSeq&lt;/code&gt; method is &lt;code&gt;INVITE&lt;/code&gt;, or if any report is &lt;code&gt;unclear&lt;/code&gt; (unparsed, missing, malformed or empty &lt;code&gt;report_blocks&lt;/code&gt;). All 26 incident calls came back &lt;code&gt;confirmed&lt;/code&gt;, each matching exactly 1 dialog with no carrier re-INVITEs and no &lt;code&gt;sendonly&lt;/code&gt;/&lt;code&gt;inactive&lt;/code&gt;. A healthy control call came back &lt;code&gt;refuted&lt;/code&gt;. The 3.7 s early-media call came back &lt;code&gt;unknown&lt;/code&gt;. Always bound &lt;code&gt;create_date&lt;/code&gt;: a day-wide version of a similar join hit my 5-minute timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did one SIP header point to a shared upstream?
&lt;/h2&gt;

&lt;p&gt;The 26 calls came through three doors: 14 on Carrier A's direct trunk, 7 via Carrier B's NL-labelled SBC and 5 via its DE-labelled SBC. Three trunks failing in the same 44 minutes suggested something shared upstream.&lt;/p&gt;

&lt;p&gt;The clue was in Carrier B's &lt;code&gt;INVITE&lt;/code&gt;s, which carry an extra header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;INVITE sip:...                              (from Carrier B's SBC)
Call-ID: &amp;lt;a UUID, Carrier B's own format&amp;gt;
X-OID: &amp;lt;an ID in Carrier A's Call-ID format&amp;gt;

INVITE sip:...                              (from Carrier A's direct trunk)
Call-ID: &amp;lt;Carrier A's format&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Carrier B's own Call-IDs are UUIDs. The &lt;code&gt;X-OID&lt;/code&gt; value has the same distinctive multi-field layout as Carrier A's Call-IDs on my direct trunk. It was on all 12 Carrier B dead calls, and that day on 651 of 651 inbound dialogs from the DE-labelled SBC and 346 of 349 from the NL-labelled one. The three without it showed one of our own extension or agent names in the &lt;code&gt;From&lt;/code&gt; display name; I didn't dig further.&lt;/p&gt;

&lt;p&gt;That doesn't &lt;em&gt;prove&lt;/em&gt; Carrier A originated them. Shared SBC software could produce the same format, and simultaneous failures don't locate a fault. But it changed what I did: I widened the watcher, which had missed the 12 Carrier B calls, and put &lt;code&gt;X-OID&lt;/code&gt; in every alert and ticket. Carrier B can confirm it against the upstream leg in its own signalling trace. Look at a full &lt;code&gt;INVITE&lt;/code&gt; from each of your trunks for similar headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I keep the watcher from going silently blind?
&lt;/h2&gt;

&lt;p&gt;A detector that finds nothing looks healthy. Each guard is a reason to investigate, not a diagnosis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty is never clean.&lt;/strong&gt; No strict-RTP lines? Check the PBX stream has &lt;em&gt;any&lt;/em&gt; recent lines; warn after 24 hours without them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness.&lt;/strong&gt; Newest PBX line older than 45 s? Skip the pass without advancing coverage. Tune it to your traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage per trunk.&lt;/strong&gt; Hourly, for each trunk with 10+ inbound calls, count how many opened a stream on a mapped address. Below half means the media moved, the log format changed, or the classifier broke. One carrier's media move left my watcher blind until this check existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep Homer off the critical path.&lt;/strong&gt; Short timeout, per-pass budget. A candidate still unsettled after 10 minutes is posted as "unconfirmed" only if bridged within 20 s of the open, in the bridge 5 s or more, and still unclosed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay.&lt;/strong&gt; A &lt;code&gt;--dry-run&lt;/code&gt; mode with &lt;code&gt;--start/--end&lt;/code&gt; runs the same rule over a past window and writes nothing. I replayed this version over the previous week before switching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What doesn't this catch?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent RTP.&lt;/strong&gt; Packets can flow with 0% loss and carry near-silence. For a complaint, check the recording's inbound leg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media that stops later.&lt;/strong&gt; A stream that locks and then dies mid-call is out of scope. So is a re-INVITE whose new stream never locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calls that never reach a bridge.&lt;/strong&gt; See the queued call above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who hung up.&lt;/strong&gt; Don't assume it was the caller giving up. In the 26 incident calls, the first &lt;code&gt;BYE&lt;/code&gt; came from the PBX side on 15 and from the carrier on 11.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Could my own firewall or NAT be the cause?&lt;/strong&gt; Possibly; this method alone can't rule it out. What pointed away from my side: 99 calls on the same trunks were fine in the same minutes, the trunks were clean the day before, and Carrier B's RTCP showed no Sender Reports on its dead calls. For certainty, capture packets on the PBX interface during an occurrence and compare with the carrier's capture.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sip</category>
      <category>voip</category>
    </item>
  </channel>
</rss>
