📞 English documentation
日本語 →

📞Layer 1: Voice Edge

The call's entry point (HGW / Asterisk / bridge). Current operations vs. Phase C v2.

The physical entry point for calls (the Voice Edge). A Hikari Denwa HGW receives the call, Asterisk passes the audio to bridge.py over AudioSocket, and OpenAI Realtime makes the decision.

Architecture

Raspberry Pi 3B (edge, on-premises) HGW (Hikari Denwa) SIP REGISTER extension Asterisk 22 (chan_pjsip) dialplan / chan_audiosocket bridge.py AudioSocket :9092 / slin 8k↔PCM 24k OpenAI Realtime classify_call to be ported (32→64bit) see Remaining Issues RTS RDS Arrowfone AMI :5039 Status → Hangup (never fires) target channel matched by AEGIS_UUID block → Hangup(21) Gate 1 / blocklist (fail-safe) allow → Dial(ALLOW_DESTINATION) normal forward unknown/empty → Dial(HUMAN_PEER) fail-open (C-2(b))
Diagram: solid = audio path (slin 8k ↔ PCM 24k). Dashed = decision→hangup (Plan B): the code that looks up the target channel via AMI Status on the AI's should_terminate remains but never fires (slated for removal, 2026-09-16); a blocklisted number is cut at Gate 1 (fail-safe), allow/unknown fall through to the dialplan (fail-open). The solid purple box is the Raspberry Pi 3B device boundary (on-premises); the HGW and OpenAI sit outside it (the bottom row shows call outcomes the Pi's dialplan executes). Note: the legacy Plan A (verdict via AstDB) is archived. Dotted = existing Rhodium features that will co-locate on the Pi after the 64-bit port.

Current operations vs. Phase C v2

Current operations (v1) voice-edge A22 (pre on-device test)
Hardware Existing equipment New Raspberry Pi 3B
OS Raspbian 9 Raspberry Pi OS Bookworm 64-bit
Asterisk Asterisk 11 Asterisk 22
SIP chan_sip chan_pjsip
Audio link (legacy path) AudioSocket → bridge.py
Setup Via ChatVoice scripts/aegis_setup.sh / systemd (asterisk.service, aegis-sip-bridge.service)
✅How failures fall (important)

Calls from listed numbers (blocklist) get an announcement and Hangup(21) at Gate 1 (route-decision=block) (fail-safe = block). The path that cuts a call on the mid-call verdict (spam) does not fire in the current code (see the AMI row under “Key parameters” below; slated for removal). Anything undecidable or any fault goes to a human (fail-open = stay in business); the final design (2026-09-16) rings the fixed phone once instead (stage 1 E01, not implemented). We do not use cutting everything off (fail-closed); it was removed in C-2(b). See the glossary for details.

Key parameters

  • AudioSocket: 127.0.0.1:9092. Asterisk sends slin (16-bit PCM, 8 kHz) (dialplan c(slin)); the bridge resamples to 24 kHz PCM. AUDIO_FORMAT=ulaw is a compatibility mode that, per measurement, is never needed (bridge _audio_format())
  • AMI: 127.0.0.1:5039. The bridge’s hangup_by_uuid (Status lookup → Hangup) is still in the code, but the path from the in-call verdict never fires (conversation_may_terminate() is always False). Slated for removal by the final design (2026-09-16)
  • OpenAI Realtime: default model gpt-realtime-2.1 (override with AEGIS_REALTIME_MODEL), connecting over /v1/realtime
  • Hand-off to a human: Dial(PJSIP/…@hgw-trunk,30) to HUMAN_EXTEN (set in kitted; empty = the called extension). The placeholders <ALLOW_DESTINATION> / <HUMAN_PEER> do not exist in the current dialplan
  • route-decision decision has 4 values (block / human / ai / take_message); empty → static fallback, unknown → a human
✅OpenAI Realtime migrated to GA

The OpenAI Realtime Beta API was retired on 2026-05-12. bridge.py is migrated to GA (default model gpt-realtime-2.1); connect / session.update accepted / classify_call relay are verified against real OpenAI. The “classify → closing line → AMI Hangup” run-through is a July 2026 record; that path does not fire in the current code (see below).

ℹ️Audio format: Asterisk → bridge is slin (16-bit PCM, 8 kHz)

Real Asterisk hands AudioSocket slin (16-bit PCM, 8 kHz, LE) (measured 2026-08-03; dialplan c(slin)). The bridge default AUDIO_FORMAT=slin takes it as-is and resamples to 24 kHz PCM. AUDIO_FORMAT=ulaw is a compatibility mode for an AudioSocket that carries μlaw; measurements show no case that needs it. The earlier text here (“μlaw 8 kHz direct-send, default pcm16”) was wrong (corrected 2026-09-16).

⚠️Gate 2 (in-call automatic hangup) does not run and is slated for removal

The path that books an AMI Hangup from classify_call’s should_terminate (Plan B / D-1) remains in the code, but conversation_may_terminate() is always False, so it never fires on any call (bridge src/bridge.py, br=a557a5b1d41d). The final design of 2026-09-16 (Plan A) removes this path; in-call decisions arrive as a reception mode in tool responses. The caller is never cut on a conversation verdict.

📌The new design (Plan A, fixed 2026-09-16) is not implemented yet

The hand-off contract is aegis-docs docs/PHONE_CALL_CONTRACT.md. Main changes (all unimplemented; each stage’s acceptance sheet says what landed):

  • The call ID is created before route-decision and also given to human-direct and announce-and-end calls (today it is created only on the AI path and never sent to the verdict)
  • Undecidable (empty, bad JSON, 401, timeout) → ring the fixed phone once (today: the static AI_ROUTE_DEFAULT fallback)
  • If nobody answers, one AI re-intake (only for calls that already went through AI); never back to a human from the re-intake
  • Messages are committed on the phone side too; unsent-to-server still counts as taken. Messages are never auto-deleted. At the cap, AI intake stops but the fixed phone still rings
  • The AI intake path is 300 s from ring (180 first AI / 30 ring / 90 re-intake are guides). No time limit once a human answers
  • Recordings are deleted from the Pi after the NAS confirms them; untransferred ones are kept within half of the SD’s free space, oldest first