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:codeand:name;:idremains only for the endpoints still keyed on it and is documented as internal. NoGuava.Campaignsfunction takes an id — pass a code or a%Guava.Campaign{}. The campaignupdatefunction is gone, having been deleted upstream. Single-field results are unwrapped:upload_contactsreturns a count andstatusa counts map. Guava.Usageis removed andconfig :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:telemetryspans 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, matchinghttpx, which defaults tofollow_redirects=False. Req had been following up to 10 redirects and rewriting POST to GET for 301–303, dropping the JSON body — so a redirectedsend_sms!/4could return:okfor 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'sTimeout(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/1closes listener sockets and waits out in-flight calls, wired to the application'sprep_stopcallback so aSIGTERMduring a rolling deploy no longer drops live calls. Guava.ready?/0for 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.Channelchildren 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-failedwas never reported; a failed connect was counted twice; open-ack timers were orphaned;conn_readysurvived 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-ackmid-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/1took the listener down; a dead socket left a channel reporting ready; a slow agentinit/1stalled 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/3is reachable underGuava.Testing, and a crashing agent under test no longer takes the test process down.- An unparseable
Retry-Afterno longer masks the real HTTP status.