Repository navigation
Releases: thevoiceguy/siphon-ai
Release list
v0.56.0
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 withRequire: 100reland 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_nameor 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 — whileforge_rtp_latch_rejected_totalstill 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.
- One final per INVITE transaction (sip-transaction 0.7.3). A 200 racing the 487
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 thataudiopus_sysbuilds declares a
cmake_minimum_requiredCMake 4 refuses, so the build script fails. CI, the release
workflow anddocker/Dockerfilenow setCMAKE_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_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
Changed
examples/pipecat-bot-pynow speaks with Deepgram Aura TTS by default
(BOT_TTS_PROVIDER=deepgram), reusing theDEEPGRAM_API_KEYits 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 OpenAIgpt-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=openairemains available, now with aBOT_TTS_MODELsetting
that defaults totts-1(median 1.64 s, worst 2.3 s) rather than the
occasionally stallinggpt-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 (includingsdks/python's
AudioSenderandsdks/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_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
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, thentransport.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 sendsclearon 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 longerdecision_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 (amarkround-trip), maps DTMF both ways to Pipecat's
native frames, and surfaces every other event as a typedSiphonEventFrame.
Requirespipecat-ai1.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, withend_call/transfer_calltools, and an--echomode (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:
ratatui0.29 → 0.30, clearinglruadvisory
GHSA-rhfx-m35p-ff5j
(RUSTSEC-2026-0002 / RUSTSEC-2026-0253, low severity):lru0.12'sIterMutviolated
Stacked Borrows. The only path to it wasratatui0.29 (no patched 0.29.x exists);
0.30'sratatui-coreuseslru0.18. The daemon is unaffected. It never links
ratatui, and the only change to what it builds isbitflags2.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 auditalso loses the unmaintained-pastewarning.
Artifacts
- Static musl binaries for
x86_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
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 oneoffset_msaxis.
All additive within protocol v1.offset_mson 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_msanchors the WAV: it is the timeline position of the
file's first sample, sotimeline_ms = recording_started.offset_ms + sample * 1000 / rate
places the recording under every other event.speech_started.bot_playing: truewhen the caller started speaking over the bot,
in every barge-in mode. Absent otherwise.playout_started/playout_stoppedbot-turn events, opt-in via
[bridge].playout_events(per-route override). A stop carriesduration_msand a
reason:completed,barge_in,cleared,muted,heldorparked. New counter
siphon_ai_playout_turns_total{reason}.- Schema regenerated; both server SDKs gain the fields and the
PlayoutStarted/
PlayoutStoppedtypes; the example echo servers log the bot turns.
Artifacts
- Static musl binaries for
x86_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
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_tagandvalidate_event_packagesliced aFromtag /Eventheader 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 throughextract_tag, so a single crafted
header from any peer could stop the process. Both sites now returnNonewhere 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 dailycargo auditfailure that had been
open onmainsince 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 (itssip-sdppin 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_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
Added
- Node health in Homer. With
[hep]shipping to a collector, each node now sends its
own health as HEPLog(0x64) chunks keyed bynode:<[node].id>, so Homer shows a
per-node timeline:node_startedat boot,node_ready/node_not_ready/
node_drainingas/readyand the drain flag change, anode_statusheartbeat every
[hep].node_status_interval_secs(new; default 60,0= off, below 5 fails the load),
andnode_stoppingat teardown, queued before the HEP worker drains. Each line carries
version, ready, draining, active calls, registrations and uptime — the
GET /admin/v1/statussnapshot plus/ready, read from the same sources so they cannot
disagree. New countersiphon_ai_hep_node_events_total{event}. Format in HEP.md's new
Node health Log chunks section.
Artifacts
- Static musl binaries for
x86_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
Fixed
- Calls now ship the HEP lifecycle Log chunks HEP.md has always described
(#604).HepTelemetry::emit_log's
only caller was thePOST /admin/v1/hep/testprobe, so a call produced SIP, RTCP,
RTP-QoS and CDR chunks and noHepProtocol::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 acall_startedline and
acall_endedline 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 readcall_idout 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 pinssip-sdpat 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/AVPFparsed as
its own profile). The forge range also brings forge-mixer 0.4.0, whose new
MixerOptions::frame_clockthe conference room setsfalseto 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_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
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_sdkERROR line that a target-named log filter mutes. HEP had the same
blind spot since #460, and CLAUDE.md §4.7 has described asiphon_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 gaugesiphon_ai_otlp_collector_up(1/0, shared by spans and logs — same
endpoint), newreason="collector_down"onsiphon_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_fullkeeps meaning what it meant. - HEP: hep-rs grew
UdpHepSink::send_failures(hep-rs #3), so a send the socket
refused —ECONNREFUSEDafter a dead collector's ICMP port-unreachable — is now
siphon_ai_hep_packets_dropped_total{reason="collector_down"}, and
siphon_ai_hep_collector_upis 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
SiphonAIOtlpCollectorDownandSiphonAIHepCollectorDownin
examples/observability/rules/alerting.yml; CONFIG.md, DEPLOY.md, HEP.md, DEV_PLAN R12
and §4.7 now describe what exists.
- OTLP: the span and log exporters are wrapped so every batch's outcome is observed.
-
The
warnlog floor is enforced, not just default
(#597). A--log/RUST_LOG/
SIPHON_AI_LOGdirective 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_rsreporting a dead
collector,opentelemetry_sdkreporting a failed export. The fail-safe held only while
nobody set a filter. Now such a directive getswarn,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 bareoff) is left alone. The same rule applies to
PUT /admin/v1/log, whose responsefilteris what was actually installed. The daemon
logslog filter activewith 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
warnthat still gets out. -
Mutual TLS on the SIP trunk: verify who is calling, and route on it
(siphon-rs #129 /
#130, pinv2026.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.
requiredfails the handshake for a peer with no certificate — it never sends a SIP
message;optionaladmits 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 bysiphon-ai check. Applies to the SIP TLS
listener only (browsers on[sip.wss]do not present client certificates).[sip.tls_client].client_cert+client_keypresent a certificate on every
outgoing TLS connection to a peer that asks — mutual TLS toward a trunk. Key must
be0600, as the listener key already is.peer_cert_sanroute 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
infowith the peer address, and the routed /
no-match lines carrypeer_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/callsrows gain
an optionalpeer_identityfield (additive — absent for every call that presented
none, so existing rows are byte-identical);print-configshowstls_client_authand
tls_client_identityundersip.
- SANs) and the SHA-256 fingerprint at
- 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.01was superseded untagged by
v2026.09.01.1). Across the whole range only three files we build
changed. forge-engine givesMediaSessiona 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 anAtomicU32so the per-packet forwarding path stays lock-free.
It is additive: every constructor seeds it from the frozen
transcoding_config.payload_type_mapthe 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-sdpdependency 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 onesip-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 inforge-conference, notforge-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 inexternal/siphon-rs, so the embedded pair matches.Verified with the full workspace suite, a
--features webrtcbuild,
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 notrace_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 exportlevelso the console and the collector can have different
appetites (narrower only — the global filter is still a floor over every
layer, whichdocs/CONFIG.mdstates 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
tracingevents of its own from inside
emit("queue full", "emitted after Shutdown"). Dispatched from within a
layer'son_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 — adebug_assertpanic in debug builds, and silently wrong
per-layer filtering in release, which is worse. So nothing calls into
the SDK fromon_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...
- The OTel batch processor raises
v0.50.1
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(fromv2026.08.21) — picks up
sip-identity 0.3.0:
PASSporT signing behind a newsignfeature
(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(from1fe94a502efa) — one
commit: aligns forge-media's own siphon-rs submodule tov2026.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 insiphon_ai_invites_totalas
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 remainssiphon_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, andPOST /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
unknownprobing signal dashboards key on. The static route table is
now a single shared source (STATIC_ROUTES) answering both
min_roleandroute_label, so a route can no longer be known to
authorization yet invisible to metrics.
Artifacts
- Static musl binaries for
x86_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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
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.
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
addsPortPool::allocate_reserving(min_free)and
MediaSessionConfig::min_free_port_pairs, which is what makes
[media].reserved_outbound_callsabove 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
gaveforge-vada types-onlyforge-coredependency 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 buildforge-engine
withdtls, so this is live wherever[media].srtp_offer = "dtls"
(0.9.4) is set. The SDPa=fingerprintstayssha-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. Thedelayed_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 vendoredsip-sdpgoes 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 momentrtp_port_rangechanged.Set
reserved_outbound_calls = Nand the inbound allocator refuses
once the pool's free pairs reachN, 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 newsiphon_ai_rtp_reserve_blocks_total
(published as a zero baseline, so "the reserve has never had to shed"
reads as0rather 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-configprints 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
503to 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 andNneeds no slack (that
took an upstream change; see the forge-media bump below). The one
caveat, indocs/CONFIG.md: the knob decides who loses when the pool
is too small, it does not make it bigger.rtp_port_rangemust still
be sized for the sum of both directions;docs/DEPLOY.mdnow 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
answers202and the port failure arrives later on the
outbound_failedwebhook). -
The SIPp harness asserts the outbound delayed-offer counter, not just
"answered" (#558). Theoutbound_delayed_uasphase 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 owncheck_itproves 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 onmissing_sdp_offer,
invalid_remote_media,media_activateor ansrtp_*label.
Artifacts
- Static musl binaries for
x86_64andaarch64(.tar.gz). - Debian packages (
.deb) foramd64andarm64—apt install ./siphon-ai_*_amd64.deb. SHA256SUMS— verify withsha256sum -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