v0.49.0

thevoiceguy/siphon-aiv0.49.0Aug 17, 2026by github-actions[bot]

Upgrading?

AI Summary

Major release introducing Sightglass, a terminal operator console, and completing the Sightglass design plan. Includes admin API enhancements like drain, recent CDRs, errors, and status endpoints, along with CDR and error ring buffers.

Key Highlights

  • Introduction of `sightglass`, a terminal TUI console for fleet management (Overview, Trunks, Calls, Rooms, Errors tabs).
  • Admin API endpoints: `POST /admin/v1/drain`, `GET /admin/v1/cdrs/recent`, `GET /admin/v1/errors`, and `GET /admin/v1/status`.
  • New `bins/sightglass` binary included in release tarballs alongside the daemon.
  • Shared `crates/admin-api-types` crate to ensure wire format stability between daemon and client.
  • Deferred reaper mechanism for SIP dialogs to fix memory leaks.

New Features

  • Sightglass operator console (TUI) with multi-node support.
  • CDR ring buffer for recent calls.
  • Error ring buffer for fleet-wide error tailing.
  • Sightglass UI tabs for Rooms and Errors.
  • Admin API endpoints for drain status, recent CDRs, errors, and node overview.

Full Release Notes

### Added

- **`POST /admin/v1/drain` and sightglass's System tab — the plan's final chunk** (DESIGN_SIGHTGLASS.md §6.5, PR 6 of 6). The daemon gains an admin-role **programmatic drain**: the endpoint fires a `Notify` the runtime's `run()` selects beside the SIGTERM future, entering the exact graceful path (503 new INVITEs, active calls finish, force-terminate at the deadline, exit) — `202 {drain_signalled, already_draining}`, idempotent, and deliberately not wired to the "second signal forces teardown" escape hatch, so repeating the POST can never kill calls. `GET`/`PUT /admin/v1/log` responses move to typed wire structs (byte-identical JSON). Sightglass gains the **System tab** (`6`): one row per node with live log filter, drain state, HEP, and version — `L` sets the tracing filter on the selected node (live, no restart), `H` emits a HEP probe, `D` starts the drain behind a deliberately loud node-named confirm; all admin-gated per node. This completes the six-PR sightglass plan.

- **Release tarballs now carry two binaries: `siphon-ai` and `sightglass`.** The release workflow builds and packages the operator console alongside the daemon for both musl targets. The `.deb` stays daemon-only on purpose — sightglass is a client tool that usually runs on an operator's workstation, not the service host; take it from the tarball.

- **A recent-CDRs ring behind `GET /admin/v1/cdrs/recent`, richer live stats, and sightglass's call history** (DESIGN_SIGHTGLASS.md §6.3/§6.4, PR 5 of 6). A ring-capturing sink joins the runtime's CDR fanout and keeps the last `[observability].cdr_ring_size` (default 50) completed calls' **CDR records verbatim** in memory — the CDR schema is the one schema, served newest-first on the readonly endpoint, working with zero `[cdr]` sinks configured and surviving SIGHUP sink rebuilds. The live-stats endpoint `GET /admin/v1/calls/:id/stats` gains **static call facts**, registered once per call beside the quality feed: `direction`, `from`, `to`, `sip_call_id`, `sample_rate`, `srtp_profile` (absent = plaintext RTP), `verstat_attest` — all additive JSON. (The design's dynamic fields — live VAD state, last DTMF, WS reconnect count — are deferred; each needs tap→registry streaming that isn't worth building unasked. The design note records the split.) Sightglass's calls tab grows a **recent pane** under the active table (ended time, from → to, hangup cause, duration, MOS — read defensively off the versioned CDR records) and the detail pane renders the new static facts. Pre-0.49 daemons degrade to an absent pane, never a down node.

- **`GET /admin/v1/status`, a completed sightglass overview, and the Rooms tab** (DESIGN_SIGHTGLASS.md §6.2, PR 4 of 6). The daemon gains a one-request (readonly-role) JSON summary — `{version, uptime_secs, active_calls, registrations: {registered, total}, draining, hep_enabled}` — the live snapshot a dashboard or deploy script wants without parsing Prometheus text; cumulative counters stay on `/metrics` (the design's `total_calls`/`hep_collector_up` fields were dropped — neither has readable state behind its metric, and duplicating counters into core wasn't worth it; the design note records the deviation). Sightglass's overview grid gains **version and uptime columns** (dash on pre-0.49 daemons), and the new **Rooms tab** (`4`; errors moves to `5`) lists conference rooms with members inline plus parked calls across the fleet — `x` ends the focused room / kicks the focused member / hangs up the focused parked call via node-named confirms (operator role), `u` retrieves the focused parked call with the optional new-`ws_url` form. Conferences/parked polls tolerate `501` (feature off ⇒ empty), and status tolerates pre-0.49 daemons — neither can mark a node down.

- **A recent-errors ring behind `GET /admin/v1/errors`, and sightglass's Errors tab reading it** (DESIGN_SIGHTGLASS.md §6.1, PR 3 of 6). A `tracing` layer captures every `warn!`/`error!` event into a bounded in-memory ring — timestamp, level, target, message with structured fields appended, and the **`call_id` of the nearest enclosing per-call span**, so entries join against `/admin/v1/calls`, the CDR, and Homer. The ring answers newest-first on the (readonly-role) endpoint; capacity is `[observability].error_ring_size` (default 256, `0` disables, >65536 fails at load — it's an operator tail, not log storage). The layer installs before config loads so config-load warnings are themselves captured, sits behind a WARN per-layer filter (zero cost on the info/debug firehose), and capture is a short mutex push on the logging thread — nothing on the audio path emits warns in steady state, and HEP-style queueing would be overkill for events this rare. One caveat documented in CONFIG.md: the reloadable global log filter gates all layers, so narrowing it below `warn` via `PUT /admin/v1/log` also narrows the ring. New metric `siphon_ai_error_ring_captured_total{level}` — a rate spike is a health signal even when nobody is watching the ring. Sightglass grows the **Errors tab** (`4`): the fleet's merged tail, newest first, node-tagged and level-colored, scoped by the node filter like every tab; a pre-0.49 daemon (no endpoint) degrades to an "endpoint unavailable" note rather than an error — and never marks the node down.

- **Sightglass grows its operator actions** (DESIGN_SIGHTGLASS.md, PR 2 of 6). The calls tab now acts, not just watches: `x` hangs up the focused call, `p` parks it, `u` retrieves (with an optional new `ws_url` — move a live call to a different WS server), `c` adds it to a conference room, and `o` opens a dial form for outbound origination — each against the existing `/admin/v1/*` endpoints, each targeting exactly one node, with the confirm modal naming that node ("hangup abc-123 **on prod-2**?") so a fleet operator never acts on the wrong box. Results surface as transient toasts carrying the daemon's own error text. Keybinds are **RBAC-aware per node**: at startup sightglass learns each token's role with two side-effect-free probes of the RBAC gate (a hangup on a sentinel call id and an empty-body originate — the gate runs before dispatch, so 403-vs-404/400 separates the roles without touching a call or dialing anything) and greys out what the role can't do; a real 403 later toasts and teaches the ceiling. `--read-only` now hard-disables all actions client-side (and skips the probes). New user guide: `docs/SIGHTGLASS.md`. Daemon untouched — this is all client-side, on endpoints that have existed since 0.6.0–0.43.0.

- **`sightglass`: a terminal operator console for one or more siphon-ai nodes** (DESIGN_SIGHTGLASS.md, PR 1 of 6). A new `bins/sightglass` binary (crate `siphon-ai-sightglass`, ratatui) that fans out to each configured node's `[admin]` listener and renders a tabbed live view — **overview** (fleet health grid + active-call sparkline), **trunks** (registration state across nodes), **calls** (fleet-unified table with both id namespaces and direction, plus a per-focused-call quality pane fed by `GET /admin/v1/calls/:id/stats` with a client-side MOS trend). Multi-node is first-class from this first cut: every record is keyed `(node, id)`, one staggered poller set per node, a down node degrades to its own dimmed row without stalling the rest, and the Node column auto-hides on single-node fleets. Fleet config is `~/.config/sightglass/config.toml` (`[[node]]` name/url/token_file/ca) or `--target` for an ad-hoc single node; `--read-only` and `--ascii` flags per the design note. Read-only in this PR — operator actions (hangup/park/retrieve/originate, node-named confirm modals, per-node RBAC-aware keybinds) are PR 2. The daemon binary is untouched: ratatui/crossterm live only in the new crate.

- **`crates/admin-api-types`: the admin API's request/response wire shapes as a shared crate**. The `/admin/v1/*` JSON shapes (`AdminCallRow`, `RegistrationRow`, `ConferenceRow`, `ParkedRow`, `DrainStatus`, the list envelopes, and the POST bodies) moved out of `siphon-ai-telemetry` into a serde-only crate consumed by both the daemon (serializer) and sightglass (deserializer), so the two cannot drift. Wire-preserving by construction: telemetry re-exports the same names at the same paths, the handlers serialize the same JSON byte-for-byte (all 79 existing admin dispatch tests pass unchanged), and new wire snapshot tests in the crate pin the exact JSON as the contract. One internal type change: `AdminCallRow.direction` is `String` rather than `&'static str` (identical on the wire; required for the deserialize direction).

---

**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.49.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