v0.41.0
thevoiceguy/siphon-aiv0.41.0Jul 24, 2026by github-actions[bot]
Upgrading?
Release notes summary
Fixes force-hangup behavior on record-routing trunks, adds HEP support for outbound calls, and improves drain reliability by gating outbound origination.
Key Highlights
- Force-hangup on record-routing trunks now releases the caller (fixes #342).
- Outbound-originated calls now emit a SIP HEP ladder (fixes #341).
- Drain now refuses outbound origination requests (fixes #343).
- Drain-forced calls now retain their CDR records (fixes #344).
Full Release Notes
### Fixed - **Force-hangup on a call answered through a record-routing trunk now releases the caller** (issue #342; fixed upstream by [siphon-rs#69](https://github.com/thevoiceguy/siphon-rs/pull/69), pin bumped `50866b1599a7` → `fbdf132a11d6`). A local teardown — `POST /admin/calls/:id/hangup`, a WS `hangup` (`server_hangup`), or a bridge-ended stop — tore down siphon's media and wrote the CDR but left the caller in **dead air** until the carrier's session timer fired (~60 s on Twilio). Only a far-end-initiated BYE (`caller_hangup`) was unaffected. Root cause was in siphon-rs: `IntegratedUAC::bye` and `bye_via_flow` were the only in-dialog senders that skipped `prepare_in_dialog_request`, so the closing BYE went out with **no `Route` headers** and the carrier's private media-gateway address as Request-URI. A record-routing edge can't correlate that and answers `481 Call/Transaction Does Not Exist`, so the BYE never reached the PSTN. Measured live on the Twilio trunk: the daemon's real BYE drew a 481, while a hand-crafted BYE carrying the two `Record-Route` hops + the dialog local URI drew a 200 and released the stranded leg immediately. This looked TLS-specific only because the local TCP test peer doesn't Record-Route — the real condition is a record-routing peer. No siphon-ai code change; the fix routes both BYE methods through the dialog route set exactly as `reinvite`/`send_update`/`send_refer_via_flow` already do (empty route set → byte-identical, so direct peers are unaffected), and sources the BYE `From` from the dialog's local URI (RFC 3261 §12.2.1.1). Load-bearing regression test upstream (`bye_via_flow_carries_route_set_and_dialog_local_uri`), CI-proven red without the fix. - **Outbound-originated calls now emit a SIP HEP ladder** (issue #341; fixed upstream by [siphon-rs#68](https://github.com/thevoiceguy/siphon-rs/pull/68), pin bumped `6fd432270bb8` → `50866b1599a7`). With `[hep]` enabled, calls the box *placed* via `POST /admin/v1/calls` produced RTCP/QoS/CDR chunks but no SIP ladder in Homer — while calls it *received* rendered the full INVITE→200→ACK→BYE. Root cause was in siphon-rs: the client connection pool (`ConnectionPool` / `TlsPool`), used for every outbound SIP connection, had zero HEP emission, so the UAC's INVITE/ACK/BYE and the responses it read all bypassed the hooks that the inbound accept loops and the one-shot senders already had. No siphon-ai code change — the emitter this daemon already installs simply now sees the outbound leg. Registration (`[[register]]`, also UAC over the pool) gains its SIP ladder the same way. Verified live: the outbound INVITE that previously produced zero HEP now emits. - **Graceful drain now refuses outbound origination** (issue #343). During drain (SIGTERM → draining), `POST /admin/v1/calls` was not gated: it returned `202` and actually placed an INVITE, dialing the PSTN even late in the drain window. The new call wasn't tracked by the drain and was orphaned when the process exited — no clean teardown, no CDR — and since origination is the one admin action that spends money, a deploy overlapping an operator- or automation-driven originate would strand a real call. New inbound INVITEs were already 503'd during drain; outbound was the gap. `originate` now consults the same `DrainFlag` the inbound routing handler uses and refuses with `503 Service Unavailable` (new `OriginateRejection::Draining`), checked first — before gateway validation or a concurrency permit — so a draining daemon returns `Draining`, not `UnknownGateway`, and sends no INVITE. - **Drain-forced calls now keep their CDR — file record *and* HEP chunk** (issue #344; needs the hep-rs bump to `91e689b`). A call force-terminated at the drain deadline computed `termination = DrainForced` but its CDR reached neither sink, so the one cause the record exists to capture was unobservable and metrics (`siphon_ai_calls_drain_forced_total`) disagreed with the CDRs about those calls' existence. Two independent races, both fixed: - **File record.** The post-controller cleanup task deregistered the call from the registry *before* emitting its CDR — but `drain_wait` polls that registry's length to decide the drain is complete. So a drain-forced call's slot could empty while its `FileSink` write was still in flight, letting teardown proceed and cancel the detached cleanup task mid-write. Deregistration now happens **last**, after `cdr_sink.emit(...)` has awaited the flush, in both the inbound acceptor and the outbound service, so the drain blocks until the CDR is on disk. A BYE retransmit racing this window still finds the entry and gets a 200 — the slot just lives a few ms longer. - **HEP chunk.** `HepWorkerHandle::shutdown` aborted the HEP UDP worker, discarding anything still queued — and the CDR HEP chunk is only *queued* by `HepCdrSink::emit`, not sent inline. It now signals [`UdpHepSink::shutdown`](https://github.com/thevoiceguy/hep-rs/pull/2) (new upstream) and awaits the worker within a 2 s grace: the worker closes its receiver and flushes the backlog before exiting, falling back to abort only if a wedged collector blows the grace. The global SIP/forge emitters hold sender clones forever, so the channel never closes on its own — closing the receiver is what makes the drain reliable. Because `MultiSink::emit` awaits both sinks, the file-record reorder above also guarantees the HEP chunk is queued before the drain completes, so the worker drain that follows always has it. The hep-rs drain is proven load-bearing by its own `shutdown_flushes_the_queued_backlog` test (a 100-packet backlog against a deliberately-slow collector, which `abort` would discard). Note that on a fast loopback neither race reproduces — the incidental teardown latency lets both writes win — so these are correctness/determinism fixes verified by construction and by the upstream unit test, not by a failing end-to-end here. --- **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.41.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 ```
Related Repositories
A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io
An open-source remote desktop application designed for self-hosting, as an alternative to TeamViewer.
Build smaller, faster, and more secure desktop and mobile applications with a web frontend.
π RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.
Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one