How it works
A single supervised loop ticks every 2 seconds. Each tick:- Queries running campaigns.
- Calculates demand per campaign using predictive pacing.
- Fair-share allocates across campaigns when budget is tight.
- Leases calls from MongoDB via an atomic status transition.
- Dials via LiveKit SIP
create_sip_participant. - On answer, screens AMD, then attaches the answered room to a free CXB Core worker.
Lease priority
Every contact is reached at least once before any retries fire:| Priority | Source | Meaning |
|---|---|---|
| 1 | Callbacks | Customer explicitly requested a callback. |
| 2 | Pending | Fresh records not yet dialled (full first pass before retries). |
| 3 | Retries | Failed records whose next_retry_at has elapsed. |
Concurrency vs carrier CPS
Two independent limits protect two different things:| Limit | Protects | Config |
|---|---|---|
campaign.config.concurrency_limit | CXB Core fleet — max simultaneous connected conversations | per campaign |
carrier.outbound_cps | Carrier — max new SIP dials per second | per carrier |
rate_limiter.py, keyed by carrier (falling back to trunk). A pending_wait() check prevents leasing more calls than the carrier can absorb in a tick.
Predictive pacing
pacing.py) plans a ringing pipeline deep enough to keep seats busy, bounded between a floor and ceiling well under the ring timeout. Cold start (fewer than ~20 completed calls, or no answer-rate signal yet) does no over-dial — it just fills free slots.
Abandon-rate brake
Abandon rate is computed over a rolling 5-minute window (metrics.py) and feeds back into pacing:
| Condition | Action |
|---|---|
| Continuous adapt | Anchors toward the 3% regulatory target (FTC/Ofcom). |
| Abandon rate > 5% | Hard halving brake so a spike does not bleed minutes before the proportional loop catches up. |
Below ABANDON_MIN_SAMPLE answered calls | Brake not applied yet — signal is not stable. |
metrics.py keeps per-campaign rolling-window answer_rate, AHT, and abandon_rate.
AMD screening
amd.py runs answering-machine detection on the answered media before handing the call to the bot. It is configured per deployment (initial silence, greeting window, beep detection, word timing). The outcome drives an action:
| Decision | Default action |
|---|---|
machine | teardown — drop the call, mark amd_machine. |
unknown | continue — attach anyway. |
human | Attach to a CXB Core worker. |
Attach-on-answer
When a call is answered and cleared by AMD,attacher.py POSTs /attach to a free CXB Core worker, which joins the already-answered LiveKit room as the bot. fleet.py polls CXB Core fleet capacity and reserves the best worker; trunks.py (OutboundTrunkResolver / TrunkResolution) resolves the outbound trunk and its CPS/burst/channels from CXB API.
State machine
Waiting/cancellable states includepending, scheduled, retry_scheduled, callback_scheduled, and leased. Active/draining states include ringing, amd_screening, attaching, dialling, and in_progress. Terminal outcomes include completed, exhausted, abandoned, and amd_machine.
Hardening
| Mechanism | File | Purpose |
|---|---|---|
| Circuit breakers | circuit_breaker.py | Per-dependency async breaker (closed/open/half-open) around fleet/SIP/API calls. |
| Stale reaper | loop.py | Bounds stale in_progress calls by attached_at + bot.max_call_duration_seconds × 1.10, falling back to a global timeout. |
Liveness /health | health.py | Loop records a tick each cycle; /health on port 8090 returns 503 if the last tick is stale, so systemd/LB can restart a wedged loop. |
/metrics | health.py, ops_metrics.py | JSON snapshot of the in-process metrics registry. |
| Smoke check | scripts/smoke_check.py | Read-only post-deploy check: settings load, Mongo connect, indexes, /health, /metrics, fleet reachability, no stale in-flight calls. |
Key files
| File | Why it matters |
|---|---|
src/dialler/main.py | Process entrypoint, health server, graceful shutdown. |
src/dialler/loop.py | Main supervised tick loop, leasing, fair-share, state machine, reaper. |
src/dialler/pacing.py | Predictive pacing formula and abandon-rate brake. |
src/dialler/metrics.py | Rolling-window answer_rate, AHT, abandon_rate. |
src/dialler/rate_limiter.py | Per-carrier CPS token bucket. |
src/dialler/trunks.py | Outbound trunk resolution and per-carrier CPS/burst/channels. |
src/dialler/sip_dialler.py | LiveKit room creation and SIP participant dialing. |
src/dialler/amd.py | Answering-machine detection screening. |
src/dialler/attacher.py | Attaches answered rooms to CXB Core workers. |
src/dialler/fleet.py | Polls CXB Core fleet capacity and reserves the best worker. |
src/dialler/circuit_breaker.py | Per-dependency async circuit breaker. |
src/dialler/health.py / ops_metrics.py | Liveness and /metrics. |
Deployments
CXB Dialler runs as a separate process per deployment that uses it, under/opt/dialler/ on each host.