The package version mirrors the Python guava-sdk version this port is reconciled to, so it may skip Python versions. See PARITY.md for the deviations that are deliberate.

Unreleased

  • Documented the Erlang/OTP floor as 25+ and widened CI to cover it. The README previously said OTP 26+, which was conservative rather than wrong: OTP 25 works, and OTP 24 does not — the HTTP dependency chain fails to start there, which nothing had tested. The CI matrix now runs Elixir 1.15–1.18 across OTP 25, 26 and 27, with the floor job pinned to the oldest combination that actually works.

0.36.0

Tracks Python guava-sdk 0.36.0.

Breaking

  • Campaigns are identified by code, not id. %Guava.Campaign{} carries :code and :name; :id remains only for the endpoints still keyed on it and is documented as internal. No Guava.Campaigns function takes an id — pass a code or a %Guava.Campaign{}. The campaign update function is gone, having been deleted upstream. Single-field results are unwrapped: upload_contacts returns a count and status a counts map.
  • Guava.Usage is removed and config :guava, usage_telemetry: is now an ignored key. The uploader was a faithful port but nothing ever fed it, so enabling it uploaded nothing. The SDK's :telemetry spans are unaffected. See PARITY.md for why the feed was not ported.

Changed — may affect a working deployment

  • HTTP redirects are no longer followed. A 3xx now raises %Guava.Error{type: :http} carrying the status, matching httpx, which defaults to follow_redirects=False. Req had been following up to 10 redirects and rewriting POST to GET for 301–303, dropping the JSON body — so a redirected send_sms!/4 could return :ok for a message that was never sent. If your base URL is fronted by something that redirects API paths, point it at the final URL.
  • The per-attempt HTTP timeout is 5s, connect and receive alike, matching httpx's Timeout(5.0). It was Req's 15s receive default with connect left on Mint's 30s. Retries are unchanged, so a hung host now costs ~27s across four attempts rather than ~127s. RAG and LLM calls keep their own longer budgets.

Added

  • Graceful shutdown. Guava.drain/1 closes listener sockets and waits out in-flight calls, wired to the application's prep_stop callback so a SIGTERM during a rolling deploy no longer drops live calls.
  • Guava.ready?/0 for a readiness probe you mount yourself; the SDK does not serve HTTP. It reflects intake channels only, so placing an outbound call cannot make a node report unready.

Fixed

Reliability work driven by repeated absolute-parity audits against the Python SDK. Each fix has a regression test confirmed to fail when that fix alone is reverted.

  • Multiple Guava.Channel children in one supervisor collided on child id, so the documented multi-channel example could not boot. Ids are now derived from the mode.
  • Guava.run([]) hung forever instead of returning.
  • Call runtimes and finished outbound channels were :permanent, so completed calls were restarted and a supervised outbound channel re-dialled the callee indefinitely.
  • Sockets: the failure counter reset on WebSocket upgrade rather than on open-ack, so backoff never escalated and reconnection-failed was never reported; a failed connect was counted twice; open-ack timers were orphaned; conn_ready survived a monitored drop, letting a command precede the handshake.
  • A silently half-open connection went unnoticed for ~15 minutes; a keepalive is now answered for or the connection is replaced.
  • The opening handshake had no overall deadline, so a peer trickling upgrade bytes could wedge a socket indefinitely — and with it Guava.drain/1.
  • A socket outlived its owner, and a transport could outlive its socket, leaking a process and a server-side connection per call.
  • A stray open-ack mid-session re-ran the handshake, retransmitting buffered messages and silently clearing the reconnect budget.
  • Auth headers are recomputed per connection attempt, so a token that expires mid-call can still reconnect.
  • A CLI OAuth refresh that failed produced an empty bearer token and re-issued the failing request on every subsequent call; it now raises with the real status.
  • Handlers: exiting, throwing, returning an unexpected shape, or returning a value the wire cannot carry all killed the call; each now logs and answers with the fallback. Expert-error wording matches upstream exactly, since the model reads it.
  • Handler commands were cast to the runtime's own mailbox, inverting wire order and dropping anything queued at session end.
  • A raising handle_call_received/1 took the listener down; a dead socket left a channel reporting ready; a slow agent init/1 stalled other ringing calls.
  • Outbound variables are validated before dialling. A searchable field with no handler reports an expert error rather than "no matches".
  • handle_session_end/3 is reachable under Guava.Testing, and a crashing agent under test no longer takes the test process down.
  • An unparseable Retry-After no longer masks the real HTTP status.