Skip to content

Releases: thevoiceguy/siphon-ai

v0.56.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 15:54
ea2efc3

Changed

  • siphon-rs v2026.09.22 → v2026.10.22.2 and forge-media v2026.09.22 →
    v2026.10.23
    , in lockstep (forge embeds that siphon-rs). No configuration, protocol
    or CDR change. What reaches the daemon:
    • One final per INVITE transaction (sip-transaction 0.7.3). A 200 racing the 487
      for a CANCEL is no longer sent after the 487, and a 2xx arriving after a 3xx–6xx on
      an outbound INVITE is dropped rather than delivered as a second final. Only a
      provisional with Require: 100rel and an RSeq draws a PRACK, and a retransmitted
      reliable 180/183 no longer draws a fresh PRACK answered with 481.
    • SIP connection pool (sip-transport 0.7.1): a TCP/TLS/WS connection the peer
      closes immediately no longer stays pooled with later sends going into a dead socket;
      a failed send removes only its own connection; a send parked on a peer that stopped
      reading no longer holds the pool's lock.
    • Outbound TLS to an IP destination (sip-uac 0.9.3) verifies the certificate
      against [sip].tls_server_name or the Request-URI host rather than the IP.
    • Inbound: sip-uas answers 422 to a Session-Expires below Min-SE (it used to
      accept it); the registrar grants an over-long Expires the ceiling rather than 400.
    • Media (forge-engine 0.7.0, forge-rtp 0.7.1): RTP is classified to a leg by exact
      address first, so a leg no longer goes deaf when the other leg's peer shares its IP
      (two peers behind one NAT, or loopback); recordings of overlapping speech are mixed
      per side instead of by arrival order, so two parties talking over each other are no
      longer recorded taking turns; a packet refused by the RTP latch logs one WARN per
      source (at most 32) instead of one per packet — 50/s per leg from anyone spraying
      the media ports — while forge_rtp_latch_rejected_total still counts each one.
    • WebRTC legs (forge-webrtc 0.8.0): ICE renomination — a session that nominated a
      TURN relay moves to a direct pair when one succeeds.

Security

  • Lockfile bumps for open RUSTSEC advisories: quinn-proto 0.11.19
    (RUSTSEC-2026-0338 … -0342, remote panics), rustls 0.23.46 (RUSTSEC-2026-0346 …
    -0349), rustls-webpki 0.103.16 (RUSTSEC-2026-0382), webpki-roots 1.0.11
    (RUSTSEC-2026-0381), ctutils 0.4.5 (RUSTSEC-2026-0385), and hickory-resolver
    0.26.3 (DNSSEC and parser advisories fixed in 0.26.2, regressions in 0.26.3)
    (#628).

Fixed

  • Building with CMake 4 (Ubuntu 26.04, current Alpine, which GitHub's ubuntu-latest
    now is): the vendored libopus that audiopus_sys builds declares a
    cmake_minimum_required CMake 4 refuses, so the build script fails. CI, the release
    workflow and docker/Dockerfile now set CMAKE_POLICY_VERSION_MINIMUM=3.5; set the
    same when building from source on such a host
    (#628).
  • The release SBOM now identifies itself. siphon-ai-<version>-sbom.cdx.json's
    metadata.component (the product the document describes) was the scanned path "."
    with no version, in every release through 0.55.1, so SBOM tooling couldn't tell which
    product or release it covered. The release workflow now passes --source-name siphon-ai --source-version <version> to syft. The component list is unchanged.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.56.0 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.55.1

Choose a tag to compare

@github-actions github-actions released this 02 Oct 14:44
28cbe21

Changed

  • examples/pipecat-bot-py now speaks with Deepgram Aura TTS by default
    (BOT_TTS_PROVIDER=deepgram), reusing the DEEPGRAM_API_KEY its speech-to-text
    already needs, and synthesizing directly at the call's 8/16 kHz. Measured on 12
    requests each: time to first audio was a median of 0.52 s (worst 0.59 s), against
    0.94 s with a 12.7 s worst case for OpenAI gpt-4o-mini-tts. On PSTN calls the
    median gap between the caller's last word and bot audio went from 1.95 s to
    1.01 s, and a goodbye that had waited 23 s started in 0.13 s.
    BOT_TTS_PROVIDER=openai remains available, now with a BOT_TTS_MODEL setting
    that defaults to tts-1 (median 1.64 s, worst 2.3 s) rather than the
    occasionally stalling gpt-4o-mini-tts.

Fixed

  • Pause-mode barge-in no longer drops bot audio after a resolution
    (#620). Resolving a
    pause arbitration (reject, confirm, or either timeout) re-pushes the retained tail and
    the post-pause audio straight into the media engine. A WS server still streaming at
    real time, which is what a correctly paced server does (including sdks/python's
    AudioSender and sdks/typescript), then accumulated behind that backlog, and the
    200 ms window (PROTOCOL.md §5.5) evicted about the pause length minus 200 ms of bot
    speech per resolution: 158 frames over 5 rejects on a live PSTN call. The window now
    tolerates the backlog the daemon itself created, shrinking as it drains and resetting
    on every flush, so PROTOCOL.md §4.11's "playout resumes where it stopped, then any
    audio streamed during the pause plays behind it" holds. Over-rate servers are still
    bounded. No configuration or protocol change.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.55.1 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.55.0

Choose a tag to compare

@github-actions github-actions released this 02 Oct 13:11
96b91f3

Added

  • Pipecat transport — siphon-ai-pipecat (sdks/pipecat/, design in
    docs/design/DESIGN_PIPECAT.md). Answer SiphonAI calls with a
    Pipecat pipeline: SiphonTransport.accept(websocket) in a
    FastAPI endpoint, then transport.input() / transport.output() in the pipeline.
    Pipecat's stock WebSocket transport sends audio at 2× real time, which SiphonAI's
    200 ms playout window (PROTOCOL.md §5.5) cannot absorb. Measured against the daemon,
    a 3 s reply lost 65 frames (1.3 s) at 2× and none with this transport, which paces
    exact 20 ms frames at real time. The transport also sends clear on Pipecat
    interruptions, answers pause-mode arbitrations (Pipecat interrupts → confirm, no
    interruption by the deadline → barge_in_reject; while one is pending the bot's
    audio is held, then resumed on a schedule shifted by the daemon's exact pause so no
    backlog builds up (#620 tracks the daemon side for other real-time servers). Pause
    mode needs a word-based Pipecat interruption rule and a longer decision_ms, see
    the README), satisfies the start deadline
    for listen-first bots, hangs up when the pipeline ends after the caller has
    heard the last audio (a mark round-trip), maps DTMF both ways to Pipecat's
    native frames, and surfaces every other event as a typed SiphonEventFrame.
    Requires pipecat-ai 1.12–1.x. No daemon or protocol change.
    transport.pipeline_params() pins only the pipeline's input rate; TTS keeps its
    native output rate and the transport resamples to the call's.
  • examples/pipecat-bot-py — Deepgram STT → OpenAI LLM → OpenAI TTS bot on the
    transport, with end_call / transfer_call tools, and an --echo mode (no
    provider keys) that CI now runs the full conformance testkit against.

Documentation

  • The Debian 13 getting-started guide moved to docs/GETTING_STARTED.md (from the
    repository root) and is linked from the README's install section.

Security

  • sightglass: ratatui 0.29 → 0.30, clearing lru advisory
    GHSA-rhfx-m35p-ff5j

    (RUSTSEC-2026-0002 / RUSTSEC-2026-0253, low severity): lru 0.12's IterMut violated
    Stacked Borrows. The only path to it was ratatui 0.29 (no patched 0.29.x exists);
    0.30's ratatui-core uses lru 0.18. The daemon is unaffected. It never links
    ratatui, and the only change to what it builds is bitflags 2.11 → 2.13 (ratatui
    0.30 requires ^2.12). Sightglass needed no code changes, and its screens render
    identically: all six tabs at 120×40 and 80×24 were diffed character for character
    against 0.29. cargo audit also loses the unmaintained-paste warning.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.55.0 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.54.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 17:57
0a8a33b

Added

  • Call timeline events (PROTOCOL.md §3.15, docs/design/DESIGN_CALL_TIMELINE.md).
    Everything a server needs to draw a LiveKit-style call timeline — a waveform with caller
    speech, bot turns and barge-ins mapped along it — now arrives on one offset_ms axis.
    All additive within protocol v1.
    • offset_ms on every moment-marking event that lacked it: mark (the estimated
      playout completion it fired at), barge_in_resolved, hold/resume,
      held/resumed, recording_started/recording_stopped/recording_failed, and
      stop. Each is stamped where the moment happens, not when the event is sent.
    • recording_started.offset_ms anchors the WAV: it is the timeline position of the
      file's first sample, so timeline_ms = recording_started.offset_ms + sample * 1000 / rate
      places the recording under every other event.
    • speech_started.bot_playing: true when the caller started speaking over the bot,
      in every barge-in mode. Absent otherwise.
    • playout_started / playout_stopped bot-turn events, opt-in via
      [bridge].playout_events (per-route override). A stop carries duration_ms and a
      reason: completed, barge_in, cleared, muted, held or parked. New counter
      siphon_ai_playout_turns_total{reason}.
    • Schema regenerated; both server SDKs gain the fields and the PlayoutStarted /
      PlayoutStopped types; the example echo servers log the bot turns.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.54.0 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.53.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 15:21
c7768cc

Added

  • [bridge].idle_keepalive (#610).
    Optional outbound RTP during the silent-server window: with the default "off" a call
    whose WS server streams nothing emits no outbound RTP at all, and some media paths
    respond to a peer gone silent by stopping their own RTP toward us — FreeSWITCH measured
    at one packet per 5 s, RTCP-only, starving the caller's inbound audio. "silence" emits
    a digital-silence frame per idle 20 ms tick, "comfort_noise" a low-level
    comfort-noise frame (the same forge generator MOH falls back to). Engagement requires
    the idle verdict to hold continuously for 250 ms (any server frame restarting the
    clock) so ordinary mid-utterance server jitter doesn't insert a fill frame, and frames
    keep flowing through mute and barge-in arbitration, where caller→server audio must
    keep arriving. Per-route override via [route.bridge].idle_keepalive; unknown values
    (global or route) fail at load. New counter
    siphon_ai_idle_keepalive_frames_total.

Security

  • siphon-rs v2026.09.05 → v2026.09.22 fixes two remote panics
    (siphon-rs v2026.09.22):
    extract_tag and validate_event_package sliced a From tag / Event header at a fixed
    byte offset to test for a prefix, which panics when that offset falls inside a multi-byte
    character. Every in-dialog request is matched through extract_tag, so a single crafted
    header from any peer could stop the process. Both sites now return None where they
    panicked, with regression tests, and siphon-rs's fuzz crate — described as running, but
    never built — now builds and runs nightly with four new targets. All deployments should
    update.
  • rustls 0.23.40 → 0.23.45 (RUSTSEC-2026-0285):
    TLS 1.3 handshake messages were accepted across encryption-level boundaries. rustls sits
    under every TLS surface here — SIP over TLS, the WS bridge, the admin listener and the
    webhook/CDR HTTP clients — so this closes the daily cargo audit failure that had been
    open on main since the advisory landed. Lockfile-only; the fixed rustls requires
    aws-lc-rs 1.18.1 / aws-lc-sys 0.45.0, which came along with it.

Changed

  • forge-media v2026.09.10.1 → v2026.09.22, siphon-rs v2026.09.05 → v2026.09.22. The
    siphon-rs roll carries the panic fixes above plus v2026.09.15 (sip-core 0.7.8 multipart
    bodies and RFC 6442 geolocation serialisation, sip-parse 0.4.0, sip-uac 0.7.2
    invite_with_options) — none of which siphon-ai calls. forge-media moves with it because
    v2026.09.22 embeds siphon-rs v2026.09.22 (its sip-sdp pin and submodule), and staying
    behind would link two copies of the SDP negotiator. Of the forge crates siphon-ai builds,
    only forge-rtp 0.7.0 (RFC 8285 extension walking), forge-sdp 0.2.2 (RFC 4796
    a=content) and forge-webrtc 0.7.0 (a second video content stream) changed, all
    additive; forge-engine, core, codecs, dtmf, vad, hep, ice, mixer and injection are
    unchanged, so the audio path is untouched. The rest of the range is video, GPU, MP4 and
    BFCP crates this project does not depend on. No wire behaviour changes.

Documentation

  • Getting started on Debian 13 (getting_started_with_siphon-ai.md): a first-run
    guide from a fresh install to a bridged call.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.53.0 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.52.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 16:28
edfa5ae

Added

  • Node health in Homer. With [hep] shipping to a collector, each node now sends its
    own health as HEP Log (0x64) chunks keyed by node:<[node].id>, so Homer shows a
    per-node timeline: node_started at boot, node_ready / node_not_ready /
    node_draining as /ready and the drain flag change, a node_status heartbeat every
    [hep].node_status_interval_secs (new; default 60, 0 = off, below 5 fails the load),
    and node_stopping at teardown, queued before the HEP worker drains. Each line carries
    version, ready, draining, active calls, registrations and uptime — the
    GET /admin/v1/status snapshot plus /ready, read from the same sources so they cannot
    disagree. New counter siphon_ai_hep_node_events_total{event}. Format in HEP.md's new
    Node health Log chunks section.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.52.0 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.51.1

Choose a tag to compare

@github-actions github-actions released this 11 Sep 14:00
bc071fb

Fixed

  • Calls now ship the HEP lifecycle Log chunks HEP.md has always described
    (#604). HepTelemetry::emit_log's
    only caller was the POST /admin/v1/hep/test probe, so a call produced SIP, RTCP,
    RTP-QoS and CDR chunks and no HepProtocol::Log (0x64) at all — Homer's call timeline
    was empty, and a node without [admin] could not produce a type-100 packet. Every
    bridged call (inbound, and outbound once answered) now ships a call_started line and
    a call_ended line carrying the termination cause and duration, correlated by SIP
    Call-ID so they thread onto the ladder. The bridge id rides the text for the join to the
    CDR and logs. Format in HEP.md's new Call lifecycle Log chunks section.
  • HEP RTCP and RTP-QoS chunks now thread onto Homer's call view
    (#603). forge-media stamped them
    with its session id, which is siphon-ai's bridge id (siphon-…), while the SIP and CDR
    chunks carry the SIP Call-ID — so Homer showed a call's ladder and CDR but none of its
    RTCP or QoS, and the only way to them was to read call_id out of the CDR and search
    by that. Every forge session is now given the SIP Call-ID as its HEP correlation id
    (forge-media #154, MediaSession::set_hep_correlation_id): inbound at accept (early and
    delayed offer), outbound as soon as the INVITE names the Call-ID — before the final
    response, so early-media RTCP is covered too. Browser (WebRTC) legs emit no forge HEP
    chunks and are unchanged.

Changed

  • forge-media v2026.09.02 → v2026.09.10.1, siphon-rs v2026.09.03 → v2026.09.05. The
    forge tag carries #154 above (forge-engine 0.6.0). siphon-rs moves with it because
    forge-sdp now pins sip-sdp at v2026.09.05, and staying behind would link two copies of
    the SDP negotiator; that siphon-rs release is only sip-sdp 0.3.2 (RTP/AVPF parsed as
    its own profile). The forge range also brings forge-mixer 0.4.0, whose new
    MixerOptions::frame_clock the conference room sets false to keep the drain-on-read
    mixing it always had. The rest is video-conferencing work (forge-rtp 0.6.0,
    forge-webrtc 0.6.0) in APIs siphon-ai does not call; no wire behaviour changes.

Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.51.1 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.51.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 14:08
5e8f02e

Added

  • A dead collector is now visible on /metrics, for both exporters
    (#596). The 0.51.0 OTLP soak
    killed the collector for 46 minutes under 200 concurrent calls and no metric moved:
    siphon_ai_otlp_log_records_dropped_total{reason="queue_full"} guards the daemon's own
    queue, which never fills against a dead collector because the SDK accepts records
    instantly and discards the batch one hop later; the only evidence was an
    opentelemetry_sdk ERROR line that a target-named log filter mutes. HEP had the same
    blind spot since #460, and CLAUDE.md §4.7 has described a siphon_ai_hep_collector_up
    gauge that did not exist.

    • OTLP: the span and log exporters are wrapped so every batch's outcome is observed.
      New gauge siphon_ai_otlp_collector_up (1/0, shared by spans and logs — same
      endpoint), new reason="collector_down" on siphon_ai_otlp_log_records_dropped_total
      counting every record in a failed batch, and new
      siphon_ai_otlp_spans_dropped_total{reason="collector_down"} for spans. All present
      from startup. queue_full keeps meaning what it meant.
    • HEP: hep-rs grew UdpHepSink::send_failures (hep-rs #3), so a send the socket
      refused — ECONNREFUSED after a dead collector's ICMP port-unreachable — is now
      siphon_ai_hep_packets_dropped_total{reason="collector_down"}, and
      siphon_ai_hep_collector_up is derived from it each 10 s sample: 0 when any send was
      refused in the interval, 1 otherwise. Derived over the window because connected UDP
      reports the refusal on the following send, so a point sample would flap. Every packet
      handed to the sink now lands in exactly one of sent / queue_full / collector_down. UDP
      caveat, documented: a black-holed collector (no ICMP back) still looks up.
    • Reference alerts SiphonAIOtlpCollectorDown and SiphonAIHepCollectorDown in
      examples/observability/rules/alerting.yml; CONFIG.md, DEPLOY.md, HEP.md, DEV_PLAN R12
      and §4.7 now describe what exists.
  • The warn log floor is enforced, not just default
    (#597). A --log / RUST_LOG /
    SIPHON_AI_LOG directive that names only targets (siphon_ai=debug,siphon=info — the
    shape of every example the repo used to ship) replaced the built-in filter, floor
    included, and silently muted every crate it did not name: hep_rs reporting a dead
    collector, opentelemetry_sdk reporting a failed export. The fail-safe held only while
    nobody set a filter. Now such a directive gets warn, prepended — it can turn crates up
    but cannot mute the ones it did not name — while a directive that states its own global
    level (info,siphon_ai=debug, or a bare off) is left alone. The same rule applies to
    PUT /admin/v1/log, whose response filter is what was actually installed. The daemon
    logs log filter active with the effective directive and its source at startup.
    --log-no-floor (SIPHON_AI_LOG_NO_FLOOR) restores the old replace-everything
    behaviour for anyone who genuinely wants silence, and says so at startup with the one
    warn that still gets out.

  • Mutual TLS on the SIP trunk: verify who is calling, and route on it
    (siphon-rs #129 /
    #130, pin v2026.08.25 →
    v2026.09.03). Until now a TLS trunk was encrypted but anonymous: the listener never
    asked for a client certificate, so nothing distinguished a partner node from anyone who
    found port 5061, and the only handles for a route were what the INVITE claimed about
    itself. Now:

    • [sip.tls].client_ca + client_auth = "optional" | "required" make the TLS
      listener request a client certificate and verify it against a PEM CA bundle.
      required fails the handshake for a peer with no certificate — it never sends a SIP
      message; optional admits such a peer (no identity) while still refusing a
      certificate that does not chain, so a fleet can roll certificates out before the
      switch is thrown. Both halves must be set together; the bundle must exist at load;
      the mode is validated at load; all three by siphon-ai check. Applies to the SIP TLS
      listener only (browsers on [sip.wss] do not present client certificates).
    • [sip.tls_client].client_cert + client_key present a certificate on every
      outgoing TLS connection to a peer that asks — mutual TLS toward a trunk. Key must
      be 0600, as the listener key already is.
    • peer_cert_san route match key (docs/DIALPLAN.md §4.6): matches when any name
      the verified certificate asserts — URI SANs (where RFC 5922 puts a SIP identity), DNS,
      IP, e-mail SANs, then the CN — satisfies the predicate. A call with no verified
      certificate has no candidate names and can never match it, so a .* regex cannot
      admit an unauthenticated caller the way it can for an absent header. A dialplan naming
      the key on a daemon whose listener never asks for a certificate is refused at load,
      naming the route — such a route could never fire. siphon-ai route-test --peer-cert-san <name> (repeatable) models it offline.
    • Observability: every INVITE on a verified connection logs peer_identity (subject
      • SANs) and the SHA-256 fingerprint at info with the peer address, and the routed /
        no-match lines carry peer_cert_names; new counter
        siphon_ai_tls_peer_identity_total{result="verified"|"none"} per INVITE on TLS/WSS
        (documented in DEPLOY.md, with the rollout recipe); GET /admin/v1/calls rows gain
        an optional peer_identity field (additive — absent for every call that presented
        none, so existing rows are byte-identical); print-config shows tls_client_auth and
        tls_client_identity under sip.
    • Not in this release: the identity is not yet on the CDR (would be v10) and
      [sip.admission] / [[trunk]] do not consult it — routing is the authorization
      point for now; the default route (or the absence of one → 404) is where an
      unrecognized certificate ends up.

Changed

  • forge-media pin v2026.08.26 → v2026.09.02 — a routine roll-up.
    Nothing in it was asked for by this project and nothing in it changes
    this daemon's behaviour; the bump is to stop the pin drifting.

    Three tags passed (v2026.09.01 was superseded untagged by
    v2026.09.01.1). Across the whole range only three files we build
    changed. forge-engine gives MediaSession a dynamic payload-type
    map — a B2BUA learns the far leg's negotiated payload types only when
    its SDP answer arrives, and a re-INVITE can change them mid-call — held
    in an AtomicU32 so the per-packet forwarding path stays lock-free.
    It is additive: every constructor seeds it from the frozen
    transcoding_config.payload_type_map the forwarding path already read,
    so a consumer that never calls the setter behaves identically, and we
    have no leg whose payload types arrive late. forge-sdp moves its
    sip-sdp dependency from a submodule-relative path to
    tag = "v2026.08.25" — the same tag this workspace pins every other
    sip-* crate to, so the lock still holds exactly one sip-sdp
    (verified; differing tags would have linked two copies of the SDP
    negotiator).

    Everything else in the range is forge-conference — DTMF dial-out,
    PIN auth, wait-for-moderator, an 80 ms RFC 2833 duplicate window. We
    do not depend on that crate (our conferencing is forge-mixer), and the
    dedup lives in forge-conference, not forge-dtmf, so this daemon's
    DTMF path is untouched.

    siphon-rs is deliberately not bumped: its newest tag is still
    v2026.08.25, which we already pin — and which is the tag this forge
    release records in external/siphon-rs, so the embedded pair matches.

    Verified with the full workspace suite, a --features webrtc build,
    and all 60 SIPp signaling scenarios.

Added

  • Log records now ship over OTLP carrying the trace context of the span
    they were emitted inside
    — [observability.otlp.logs], off by default
    (#589).

    Since 0.22.0 the daemon exports one trace per call, and since 0.23.0 that
    context reaches the WS server. Logs never came along: a line scraped from
    journald carries no trace_id, because the context lives in the process
    and is gone by the time the text is written. Only an in-process bridge can
    attach it. Now opening a call's trace in Tempo / Jaeger / Cloud Trace shows
    the lines that explain each span, rather than sending you to a separate log
    search to match on Call-ID and timestamp by hand.

    Same endpoint, timeout and resource attributes as the span pipeline, with
    its own export level so the console and the collector can have different
    appetites (narrower only — the global filter is still a floor over every
    layer, which docs/CONFIG.md states rather than papers over). Enabling it
    without [observability.otlp] is a fatal config error: with the span layer
    off nothing attaches a context, so every record would ship uncorrelated —
    which is exactly what journald already gives you.

    Two bugs found by running it against a real collector rather than
    trusting the wiring:

    • The OTel batch processor raises tracing events of its own from inside
      emit ("queue full", "emitted after Shutdown"). Dispatched from within a
      layer's on_event, a nested event runs a second filter pass over the
      same thread-local state the outer event is still using and leaves its
      bits behind — a debug_assert panic in debug builds, and silently wrong
      per-layer filtering
      in release, which is worse. So nothing calls into
      the SDK from on_event: the layer stamps the trace context, which must
      happen there while the span's context is still current, and hands the
      record to a bounded queue; a worker th...
Read more

v0.50.1

Choose a tag to compare

@github-actions github-actions released this 24 Aug 20:27
3008ad3

Two observability blind-spot fixes and routine upstream pin rolls.

Both fixes came out of validating the 0.50.0 getting-started flow: the
[[trunk]] gate's 403s — the exact scanner traffic the gate exists to
shed — were invisible on /metrics, and four 0.49.0 admin endpoints
reported as endpoint="unknown". No protocol change (still 1), CDR
still v8, no new config keys. Drop-in upgrade; alerting can now key on
siphon_ai_invites_total{result="rejected_trunk"}.

Changed

  • siphon-rs pinned to v2026.08.24 (from v2026.08.21) — picks up
    sip-identity 0.3.0:
    PASSporT signing behind a new sign feature
    (siphon-rs #123),
    the provider-side complement to the ES256 verification SiphonAI already
    uses. SiphonAI does not enable the feature, so verification behaviour is
    unchanged — this makes outbound STIR/SHAKEN signing (long demand-gated)
    buildable against the current pin when it's picked up.
  • forge-media pinned to c277860cd123 (from 1fe94a502efa) — one
    commit: aligns forge-media's own siphon-rs submodule to v2026.08.24,
    keeping both pins on the same upstream revision (the media path sees
    siphon-rs through that submodule).

Fixed

  • [[trunk]]-gate 403s now count in siphon_ai_invites_total as
    result="rejected_trunk"

    (#564). The allowlist gate runs in the SIP
    dispatch layer before the acceptor, whose paths owned every increment of
    that counter — so the INVITEs the gate sheds (scanner traffic hammering
    the SIP port from non-allowlisted sources) were invisible on /metrics
    and unalertable without log-pipeline tooling. Per-peer detail stays in
    the audit stream and SIP ring; the new label is alertable directly.
    Digest-auth 401s are deliberately still not in this counter (a challenged
    INVITE retries with credentials and is counted then) — that gate's
    brute-force signal remains siphon_ai_sip_auth_total{result="failed"}.

  • Four admin endpoints no longer report as endpoint="unknown" in
    siphon_ai_admin_requests_total

    (#565):
    GET /admin/v1/status, GET /admin/v1/errors,
    GET /admin/v1/cdrs/recent, and POST /admin/v1/drain — the 0.49.0
    sightglass endpoints — got authorization arms but no metric-label
    arms, so their traffic lost per-endpoint visibility and polluted the
    unknown probing signal dashboards key on. The static route table is
    now a single shared source (STATIC_ROUTES) answering both
    min_role and route_label, so a route can no longer be known to
    authorization yet invisible to metrics.


Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.50.1 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS

v0.50.0

Choose a tag to compare

@github-actions github-actions released this 23 Aug 01:50
8829390

One new config key and one dependency bump that reaches the wire.

[media].reserved_outbound_calls closes the last gap the load plan found:
inbound and outbound calls draw RTP ports from one pool, first-come-first-served,
so on a node that both answers and originates an inbound surge could starve
origination completely with no inbound-side symptom. Set it and the inbound
allocator stops at a floor, leaving the rest to origination. Default 0 is the
old unreserved behaviour, so upgrading changes nothing until you set it.

⚠️ DTLS-SRTP deployments read the forge-media bump below before upgrading.
The certificate moves from RSA-2048 to ECDSA P-256, which is live wherever
[media].srtp_offer = "dtls" is set. Almost every peer prefers P-256 already —
it is what browsers present — but a peer that can only do RSA key exchange
would newly fail.

No protocol change (still 1), CDR still v8. Adds one config key, two
metrics, and a --inspect-config line. Wants a restart to take effect.

Changed

  • forge-media pinned to 1fe94a502efa — nine commits, three of them
    filed from this project. #120
    adds PortPool::allocate_reserving(min_free) and
    MediaSessionConfig::min_free_port_pairs, which is what makes
    [media].reserved_outbound_calls above an exact floor: the check now
    happens inside the allocator's own critical section instead of as a
    free-pair read taken just before it, so concurrent INVITE setup can no
    longer dip below the reservation. Both land in the same release, so no
    published build ever had the softer version.
    #122 fixed
    forge's broken-link CI step (it grepped for a word rustdoc never
    prints, so it had passed unconditionally while eleven unresolved links
    accumulated) and #123
    gave forge-vad a types-only forge-core dependency so two of them
    could be live links. Neither changes anything we compile.

    One upstream change reaches our wire without us asking for it:
    forge-media #117 + #118 move the DTLS-SRTP certificate from RSA-2048
    to ECDSA P-256
    and offer ECDHE-ECDSA first. We build forge-engine
    with dtls, so this is live wherever [media].srtp_offer = "dtls"
    (0.9.4) is set. The SDP a=fingerprint stays sha-256 — only the key
    type, and therefore the hash value, change — and P-256 is what browsers
    and webrtc-rs present, so it is the more interoperable default as well
    as ~100× faster to generate. A peer that can only do RSA key exchange
    would now fail where it previously worked. The delayed_offer_dtls
    SIPp scenario covers the handshake.

    The remaining commits (#116, #119, #121) are WebRTC/ICE work in
    forge-webrtc / forge-ice, neither of which we depend on. #115 bumps
    forge-media's own siphon-rs submodule to v2026.08.19; we pin siphon-rs
    directly at v2026.08.21 and the two stay separate package sources, but
    the version skew closed — forge's vendored sip-sdp goes 0.3.0 → 0.3.1,
    matching what our tag carries.

Added

  • [media].reserved_outbound_calls: hold RTP ports back for
    origination
    (#556). Inbound and outbound draw port pairs from one
    pool, first-come-first-served, with no reservation between them, so on
    a node that both answers and originates an inbound surge starves
    origination completely — and there is no inbound-side symptom while
    it happens. Measured on 0.49.9 with the pool shrunk to 60 calls and 50
    inbound + 20 outbound asked for: inbound established 50/50 and
    stayed healthy for the whole window while 10 of 20 originates
    failed
    (test-harness/load/RESULTS-0.49.9-mixed-and-soak.md §2). The
    starved direction is usually the one with a deadline attached — a
    scheduled callback or a notification, versus a caller who will redial —
    so the default allocation is the opposite of how an operator would
    prioritise it, and the only lever was [sip.admission]: a global
    inbound concurrency cap standing in for a pool reservation, with the
    two numbers drifting apart the moment rtp_port_range changed.

    Set reserved_outbound_calls = N and the inbound allocator refuses
    once the pool's free pairs reach N, leaving the rest to origination,
    which is not gated. Counted in calls, not ports (one call is one pair).
    The refusal reuses the exhausted-pool path from #554/#555 unchanged —
    503 Service Unavailable + Retry-After, counted as
    siphon_ai_invites_total{result="rejected_capacity"} — deliberately:
    a caller must not be able to tell "we are empty" from "the rest is
    spoken for", since both mean try another node. The split is
    operator-side, on the new siphon_ai_rtp_reserve_blocks_total
    (published as a zero baseline, so "the reserve has never had to shed"
    reads as 0 rather than as a missing series) alongside
    siphon_ai_rtp_reserved_outbound_calls, the configured threshold as
    a gauge. Startup logs the split (pool_calls / reserved_outbound_calls
    / inbound_calls) and --inspect-config prints it.

    Default 0 — unreserved, byte-for-byte the pre-0.50 behaviour.
    Restart-required. A value at or above the pool's capacity fails at load
    with the capacity in the message: reserving the whole pool would answer
    503 to every INVITE forever, which is a concurrency cap wearing a
    media knob's clothes. The floor is exact — it is evaluated inside
    the RTP pool allocator's own critical section, so no amount of
    concurrent INVITE setup can dip below it and N needs no slack (that
    took an upstream change; see the forge-media bump below). The one
    caveat, in docs/CONFIG.md: the knob decides who loses when the pool
    is too small, it does not make it bigger. rtp_port_range must still
    be sized for the sum of both directions; docs/DEPLOY.md now says so
    under its own heading, with the measurement and the reason an operator
    watching HTTP status codes sees none of it (POST /admin/v1/calls
    answers 202 and the port failure arrives later on the
    outbound_failed webhook).

  • The SIPp harness asserts the outbound delayed-offer counter, not just
    "answered"
    (#558). The outbound_delayed_uas phase drives a full
    RFC 3264 delayed offer — offerless INVITE out, the peer's offer in its
    2xx, our answer in the ACK — but asserted only
    siphon_ai_outbound_calls_total{result="answered"}, which any
    originate satisfies. siphon_ai_outbound_delayed_offer_total (#406) is
    the counter that says the negotiation itself succeeded, and nothing in
    CI asserted it, so its documented result set could rot unnoticed. The
    scenario's own check_it proves the ACK carried an m-line; this adds
    the daemon's side of the same claim, and catches an originate that
    answers while the negotiation lands on missing_sdp_offer,
    invalid_remote_media, media_activate or an srtp_* label.


Artifacts

  • Static musl binaries for x86_64 and aarch64 (.tar.gz).
  • Debian packages (.deb) for amd64 and arm64 — apt install ./siphon-ai_*_amd64.deb.
  • SHA256SUMS — verify with sha256sum -c SHA256SUMS.
  • *-sbom.cdx.json — CycloneDX SBOM.
  • SHA256SUMS.cosign.bundle — cosign keyless signature over the checksums.
  • Container: ghcr.io/thevoiceguy/siphon-ai:v0.50.0 (multi-arch, cosign-signed).

Verify the checksums signature:

cosign verify-blob --bundle SHA256SUMS.cosign.bundle \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'release.yml@' SHA256SUMS