# Upgrading to 0.10.0
0.10.0 is a reliability release: 37 audited defects fixed across the Bot core,
the TDL runtime, and the mix tasks. Most fixes are invisible. This guide lists
every public call whose **observable behavior changed** and what to do about it.
## Sexy.Bot
### `Sexy.Bot.start_link/1` — fails fast on bad config
* **Before:** `token: nil` (unset env var) or a typo'd `session:` module started
a green supervision tree that silently never worked.
* **Now:** raises `ArgumentError` at boot for a nil/empty token; raises if the
session module can't be loaded.
* **Do:** nothing if your config is correct. If your deploy relied on the bot
booting without a token, set `BOT_TOKEN` before start.
### `Sexy.Bot.send/2` with a list — returns per-item results
* **Before:** `send([a, b, c])` returned `:ok`, discarding every response.
* **Now:** returns the list of Telegram responses in order, so partial failures
(blocked users, rate limits) are visible.
* **Do:** if you pattern-matched `:ok = Sexy.Bot.send(list)`, match the list
instead: `results = Sexy.Bot.send(list)`. Single-object `send/2` is unchanged.
### Rate limiting — automatic single retry
* **Before:** a 429 from Telegram was logged and lost.
* **Now:** the send is retried once after the `retry_after` interval Telegram
asks for. The call **blocks** for that time (seconds).
* **Do:** nothing. For large broadcasts, still check per-item results.
### `Sexy.Bot.notify/3` — button failure is no longer silent
* **Before:** if the message was sent but the dismiss/navigate buttons could not
be attached (rate limit, `callback_data` over 64 bytes), `notify` returned
`"ok" => true` and the user got an undismissable notification.
* **Now:** returns that error map (`"ok" => false`) with `"result"` still
holding the sent message, so `message_id` stays available.
* **Do:** if you check `notify(...)["ok"]`, decide how to handle the partial
case; `["result"]["message_id"]` is present either way when the send worked.
### API error returns — error maps instead of exceptions
* **Before:** a non-JSON response (e.g. an HTML 502 from a proxy during a
Telegram outage) raised `Jason.DecodeError` in *your* calling process; some
transport errors raised `Protocol.UndefinedError` inside the error handler.
* **Now:** every `Sexy.Bot.Api` method returns the documented
`%{"ok" => false, "description" => ...}` map for any transport/JSON failure.
* **Do:** remove any `rescue Jason.DecodeError` you added around send calls.
Note: transport error descriptions now use `inspect/1`
(`"HTTP error: :timeout"` instead of `"HTTP error: timeout"`).
### Callback query format — dashes in values now parse
* **Before:** `Sexy.Utils.split_query/1` split pairs on every `-`, silently
corrupting values containing dashes: `"id=-1001234-page=2"` parsed as
`%{id: "", page: 2}`. The library's own `notify(..., navigate: ...)` flow was
affected.
* **Now:** `-` splits pairs only when followed by `key=`. Negative IDs, UUIDs,
and dates in values round-trip correctly. Old callback buttons already in
chats keep parsing the same (or better).
* **Do:** nothing, unless a value must contain the literal sequence `-key=` —
that sequence is reserved as the pair separator; `=` in values remains
unsupported.
### Session callbacks — optional means optional
* **Before:** `handle_poll/1` was documented optional but dispatched
unconditionally (crash per poll update if missing); `handle_transit/3` was
invoked unguarded, *after* the message was already deleted.
* **Now:** all optional callbacks are guarded. `/_transit` verifies the handler
exists before deleting anything. `handle_poll/1` now also receives
`%{poll_answer: ...}` updates (individual votes), which were previously
dropped as unknown.
* **Do:** if you implement `handle_poll/1`, be ready for both `%{poll: ...}`
and `%{poll_answer: ...}` shapes.
### Delivery semantics (informational)
The poller now survives malformed updates, network noise, and handler crashes,
and the update offset survives poller restarts. Delivery remains
**at-least-once**: a batch can be re-dispatched after a crash in the
confirmation window. Keep payment handlers idempotent (deduplicate by
`update_id` or `telegram_payment_charge_id`).
Updates of the **same chat are now processed in order** (partitioned dispatch
keyed by chat id) — previously all updates ran as unordered concurrent tasks
and two quick messages from one user could be handled in reverse. Different
chats still run concurrently. Note the ceiling: a slow handler delays other
chats that hash into the same partition.
## Sexy.TDL
### `Sexy.TDL.open/3` — honest returns
* **Before:** opening a session name that was already running silently clobbered
the running session's registry entry and started a second tdlib on the same
database. Opening with a broken `:tdlib_binary` returned `{:ok, pid}` and the
failure event went nowhere.
* **Now:** returns `{:error, {:already_started, pid}}` for a duplicate name and
`{:error, reason}` (synchronously) when the port can't be opened.
* **Do:** handle the two error tuples if you retry opens.
### `Sexy.TDL.close/1` — actually closes
* **Before:** `close/1` stopped the session supervisor, which the
`DynamicSupervisor` then immediately resurrected: the session survived as a
zombie or crash-looped the whole account supervisor. Closing a stale session
could exit the caller with `:noproc`.
* **Now:** the session is terminated for real (`terminate_child`), never
resurrected, and `close/1` never exits the caller.
* **Do:** nothing — `close/1` now does what its docs always claimed.
### Port death — automatic restart, then permanent stop
* **Before (no proxy):** tdlib dying was *undetectable* — transmits silently
vanished, no event, no restart. **Before (proxy):** detected but the session
stayed a zombie forever.
* **Now:** the Backend/Handler pair restarts with a fresh port automatically
(up to 5 times in 30s). You still get `{:system_event, :port_exited, status}`
before each restart. A session that keeps failing dies alone — it does not
cascade into other sessions. Its registry entry is cleaned up automatically.
* **Do:** monitor the pid from `open/3` (or watch for repeated `:port_exited`)
if you want to re-open persistently failing sessions yourself.
### System/proxy events — delivered directly
* **Before:** `{:system_event, ...}`/`{:proxy_event, ...}` were relayed through
the Handler and were lost on cold start and during restarts.
* **Now:** the Backend sends them straight to your `app_pid`. Same tuples, same
`handle_info` clauses — they just actually arrive now.
* **Do:** nothing.
### Supervision shape
`Sexy.TDL`'s children now use `:rest_for_one`: if the session Registry crashes,
dependent processes restart into a consistent empty state instead of running as
zombies against an empty table. After such a reset (or any session supervisor
death) re-open your sessions — entries no longer linger with dead pids.
### `Sexy.TDL.transmit/2` — meaningful return value
* **Before:** returned the raw message tuple from `Kernel.send/2` (useless), and
"successfully" wrote into dead ports (the data silently vanished). During a
Backend restart it exited the caller with `:noproc`.
* **Now:** returns `:ok` when the command was actually written to the port,
`{:error, :no_backend}` / `{:error, :no_port}` otherwise. Writing to a dead
port fails loudly and triggers the pair restart.
* **Do:** if you matched on transmit's return, match `:ok` now.
### Inline keyboards in received messages
`vector<vector<T>>` fields (e.g. `ReplyMarkupInlineKeyboard.rows`) are now
recursively deserialized: inner elements arrive as `%Sexy.TDL.Object.*` structs
instead of raw string-keyed maps. If you worked around this by matching raw
maps, switch to matching the structs.
## Mix tasks
### `mix sexy.tdl.setup`
No longer offers to generate types — `Sexy.TDL.Object`/`Method` are bundled
with the library. Prompts fail with a clear message instead of a crash when run
without an interactive terminal.
### `mix sexy.tdl.generate_types`
Still available for regenerating types when a new TDLib version ships — run it
**inside the sexy repository (or a fork)**. It now refuses to run in a consumer
project (the generated modules would duplicate the ones compiled in the `:sexy`
dependency and break `mix release`); `--force` overrides. Generation is atomic:
a malformed `types.json` no longer destroys the previous files. Doc text from
`types.json` is escaped, closing a compile-time code-execution hole.
## Compatibility
Declared Elixir support (`~> 1.14`) is now real: the accidental use of an
Elixir 1.17+ guard was removed, so the library compiles on 1.14–1.16 again.