• v1.2.2 203bcc86b1

    ann released this 2026-11-08 02:42:26 +00:00 | 130 commits to develop since this release

    Signed by ann
    SSH key fingerprint: SHA256:W5M34SUfd1n6UVfu3dIKsi8wVTbvCykT2F7caAghIFU

    libtacky 1.2 · 2026-11-02 → 2026-11-08 · tag v1.2.2

    The 1.2 line, released: 1.2.0 is its final, 1.2.2 its latest patch; the release candidates and betas before 1.2.0 follow below. The API is stable; hosts regenerate their bindings from schema/tacky.schema.json. Test results for each version are in its tag message.

    1.2.2 — 2026-11-08

    Built on 1.2.1 (8919dc12), with the same toolchain. A patch: auto-download from contacts only, destroyed rooms that came back (interop issue 1) and room joins across a bad link.

    Host-visible

    • attachment_autofetch / attachment_autofetch_stickers: contacts (unset, the default of both), never or anyone (new, with a warning: hosts should not offer it); a stored everyone is rewritten as contacts; stickers came from everyone by default before.
    • Under contacts a room's occupant is no contact; a service of the account's own server (the sticker catalogue's pubsub service) counts as one.
    • New muc <Gone>: a room the library would have joined by itself answered item-not-found or gone, so it was not joined (nor created again).
    • A destroy clears the room's bookmark autojoin and keeps the bookmark; destroy_room tells <Destroyed> once and leaves the room.
    • muc join's 60 seconds run on a live stream (offline: iq -offline-timeout; none while suspended).

    Fixes

    • C34: autojoin asks the room's disco#info first; a destroyed room is no longer recreated empty on every sign-in.
    • A room joined while the link was down, or just before it went, is no longer given up as silent when the link comes back.
    • Catch-up: room walks wait for every room peek still out, so the newest room still walks first when joins peek late.
    • C3.1: a draft holding a character XML cannot carry is published without it; a refused write is logged, not a bgerror.

    1.2.1 — 2026-11-08

    Built on 1.2.0 (1dae0a6c), with the same toolchain. A patch: forge issues #20-#23, modern XMPP, the fixes of interop issue 1 (AI interop runs against slixmpp) and the duplicates the 1.2.0 rigs left open.

    Host-visible

    • message forward answers the forwarded row's timestamp (timestamp_us), as send does.
    • bookmarks item/remove: a waiting write superseded by a newer one of the same room answers "".
    • groupcall start/invite in an OMEMO chat with no OMEMO 2 device: {TACKY GROUPCALL ENCRYPTED} (answers withheld with a <Warning>); meetings there: MEETINGS ENCRYPTED.
    • MucSubscriber jid / <SubscriberChanged> user_jid "" in a nick-only room; call_invite state joined only while this device is in the call (else elsewhere or pending).
    • avatar publish refuses images over 72 KiB ({TACKY AVATAR TOO_LARGE}); mam query answers count; roster <Subscribe> -reason; room names from disco#info.
    • New: vcard set_visibility/visibility (contacts, basic, public); setting legacy_file_metadata; the software version answered is the release.

    Fixes

    • Issues: #20 bounce cleared by a later receipt, #21 room names, #22 subscribe reason, #23 expired previews.
    • A room mention's notification no longer fails on a stored row without an author.
    • Modern XMPP: no <priority/> 0, avatar cap, profile visibility.
    • Security S1-S13: call invites inside the OMEMO 2 envelope, meetings refused in OMEMO chats, chat state/receipt request/markable sealed, legacy SIMS metadata switch, fake-room OMEMO downgrade, CAPTCHA images checked, spans strip, gateway roster steps, geoloc stop order, OTR ids without expr, link ports/queries, no secrets in <Ack> or the console.
    • Correctness C1-C20 and minor M2-M25 of the same issue; docs brought up to date.
    • Duplicates and ordering: Matrix double send, a MongooseIM re-archived 1:1 message, bookmark writes one per room at a time (a retract answered item-not-found is done), replies and backups by wire_id, store renames never split a main file from its -wal.

    1.2.0 — 2026-11-07

    Built on 1.2.0-rc5 (fba31ad7), with the same toolchain. The final of 1.2.

    What 1.2 brings (for a host on 1.1.x)

    • Catch-up reworked: peek, then walk (newest page drawn first, then the archive in order); no message lost under skew, drops or trims (fuzzer-checked); a short absence costs about what it missed; a phone's lock and unlock keep the stream.
    • Read points: the call tier follows XEP-0490 from the full client's read points, so a chat read elsewhere withdraws a locked phone's notice.
    • Connection: XEP-0495 Happy Eyeballs, XEP-0509 IAP, Caps 2.0, far fewer requests per connect.
    • Matrix end-to-end encryption through a urn:tacky:matrix-e2e:0 gateway (tacky-olm, vodozemac 0.11.2), experimental and off unless matrix_e2e_enabled is set; the gateway ships in 1.3.
    • Polls are urn:fluux:poll:0; XEP-0232 software information (off by default); message delete for chosen rows of this device's history.
    • The stranger rule (someone this account wrote to or asked to subscribe to is no stranger) and calls_from_strangers; the installation-wide offline setting is gone (account disable per account).
    • Also: video added to a running call, screen share on Linux, bot/channel rows and tag keys, normalised JIDs, a row's id (stamp keys deprecated, removed in 1.3); Rust 1.99.1, floor 1.91.

    Since rc5

    • The three reviews of 2026-11-07: a ? in a localpart no longer passes for a domain, OMEMO keys a sender by the prepared JID, backup import judges the prepared JID; iq ids and the ack hold carried across a tier switch, timed-out requests withdrawn; and the Tcl review's Lows.
    • Stream: an over-cap stanza is skipped to its foot and acked instead of ending the connection (a stanza could loop every device of an account); past 16 MiB it is CLOSED oversize.
    • A refusal after the call returned is an error <MethodError> for a call with -command and no -onerror (35 modules).
    • Catch-up: walks resume after a stored message, not our clock; an account or room without an archive gets no holes; ties on a millisecond stamp keep their order.
    • Rooms: corrections, retractions and reactions taken by nick where the room gives no occupant ids (MongooseIM); the reserved nick asked at the join; our own affiliation change in the member list on the room's result.
    • Bookmarks kept locally until the server answers a write; avatars fetched once per hash; group-call legs whose keys came late redialled.
    • The stranger rule over a catch-up page as a whole; a deleted message replayed from another resource stays deleted.
    • PEP, registration and interop fixes from the runs on Prosody, ejabberd, MongooseIM and Openfire; the integration rigs now cover all four.

    1.2.0-rc5 — 2026-11-07

    Built on 1.2.0-rc4 (6f79790c), with the same toolchain.

    Catch-up

    • No messages lost under clock skew, drops and trims: frontier written at the start, a room's start stored and its peek gap judged by id, capped walks bracket the chats they did not reach; a model-based fuzzer keeps it so.
    • The review's fixes: a held message keeps its own hole, one top fetch per chat while no catch-up covers it, a rejected frontier replaced.
    • A walk whose id the archive refuses (a moderated message) goes on from up to 20 earlier stored ids instead of the archive's start (8,780 rows for one room).
    • A chat opened under the catch-up draws its stored rows at once (no "Loading" over one message after an unlock).

    Connect volume

    • A short absence costs about what it missed: bounded peeks from the newest stored message, no history replay for a room read here (5 min away was 1000+ stanzas and 10 MB per connect).
    • A fresh session no longer replays the old session's unacked room joins; it rejoins through muc join.
    • Synced lists (trust, encrypted chats, pins, tags, mutes, drafts) re-opened rather than fetched again; the OMEMO trust list compacted (200 devices' history with 10 left keeps 39).
    • No urn:xmpp:stickers:0+notify (84 KB of contacts' packs per sign-in); our devices follow our own node by an explicit subscription.

    Connection

    • account unregister succeeds only on the ends a removal comes with; a "Too many unacked stanzas" policy-violation keeps our queue.

    Matrix

    • tacky-olm on vodozemac 0.11.2 for RUSTSEC-2026-0451 (Olm ratchet keys left in freed memory); commands and pickles unchanged.

    Read points

    • The call tier starts from the read points the full client knew, so a chat read elsewhere while the stream was down is told as notify <SealedRead> at the lock.

    Stores

    • The capitalised-JID migration no longer fails the open; any failure is undone and retried. First open of a 60,000-row store 440 ms instead of 1,520 ms.
    • A store file that cannot be renamed at the start (Windows, held open) opens under its old name; an older backup in capitals restores under the prepared JIDs.

    1.2.0-rc4 — 2026-11-06

    Built on 1.2.0-rc3 (28ab9df0), with the same toolchain.

    Addresses

    • Bare JIDs normalised on entry (RFC 7622): stored and emitted in one spelling; capitalised accounts and chats migrated once at the store's first open, duplicate chats merged; an invalid address refused at the API (issue 19).
    • Hosts: events carry the normalised -acc and chat JIDs; account list is the source of truth.

    Messages

    • Message.id: an opaque id per row, kept while the row lives; every method naming a message takes -id, every event naming a row carries it. -timestamp and newtimestamp deprecated until 1.3.
    • A forward of a forward keeps the original author, chat and date.
    • An encrypted picture forwarded into a plain chat opens (<Edited> once its url is on the row).

    Connection

    • A stanza the server ends the stream over is not replayed on every new stream; its row fails with the condition (sm <Refused> -condition); the redial backs off across the same error.
    • Stream management counts and replays stanzas only.
    • account unregister answers on ejabberd; the account is disabled after.
    • SCRAM over WebSocket without -PLUS says gs2 n (ejabberd refused wss:// sign-ins); XEP-0440 rules 4 and 5 over SASL2.
    • happy_eyeballs and iap reach the connection; XEP-0478 over-limit results answered policy-violation.
    • SASL2 + Bind 2 keeps the pre-auth features (roster versioning, pre-approval); roster gets carry ver; gateways and accepted invitations approved when their subscribe arrives.

    vCard and backup

    • No echo of a server-converted vCard4 item; vCard4 publishes one at a time.
    • XEP-0227 export/import of vcard-temp as <user/>'s child; vCard4 node at the profile's access.

    Wire

    • A forward's inner message in jabber:client (XEP-0297); empty uploads refused before a slot request (XEP-0363); a peer cannot choose its stanza-id (XEP-0359).

    Rooms

    • Our other device under another nick in a room is us; a nick changed through the bookmark on another device follows here.

    Presence

    • The call tier says the show, status and priority the user set, not a plain available.

    Catch-up

    • A start on ejabberd reads the account's synced nodes once, not twice: 9 gets and 71 KB instead of 20 and 142 KB with a 47 KB trust list; the archive's peek still goes first.

    Matrix

    • The Matrix lists are read only when the account speaks Matrix (a gateway or Matrix keys); an account without asks nothing.

    Startup

    • The call tier's throwaway store is made in the cache directory, not the system's temporary one: where an Android app cannot write that, every cold start of a split store failed its first unlock.

    Certificates

    • A certificate pinned with trust_certificate (or accepted with ignore_certificate_errors) holds after a restart; the next process no longer refused it as UnknownIssuer every five seconds.

    1.2.0-rc3 — 2026-11-05

    Built on 1.2.0-rc2 (fe57dd98), with the same toolchain.

    Catch-up

    • Peek, then walk: at every <Ready> and <Resumed> the account's
      archive is read newest page first (250, and up to two pages more while
      a page holds only our own devices' rows), filed and told at once, then
      walked forward from the anchor in order as before. On a day's backlog of
      the user's mix the newest message from a peer draws in 188 ms after one
      archive answer, against 3.7 s after 23 pages; the account's
      <CatchupDone> stays where the walk ends.
    • An OMEMO message the peek cannot open out of order is stored as a
      placeholder (Message.unreadable sealed) and opened in place
      (<Edited>) when the walk reaches it in order; it counts and alerts
      then, once. The ratchet is asked what an unwrap would cost before any
      key is derived, so reading ahead never spends the keys of the messages
      still to come.
    • Holes go below the peek's rows where the archive is unknown, so paging
      back fills before the walk arrives; a complete walk takes them back. The
      walk's frontier is stored with the page it follows (one commit), so a
      drop, a kill or a restart mid-walk continues where it stopped.
    • Rooms: a joined room's newest page is read at the join and drawn under
      the same rule; its walk waits in line behind the account's instead of
      starting beside it, and keeps its own frontier across drops.
    • Quiet while syncing: markers, XEP-0490 publishes, vCard, disco, poll,
      gateway, thumbnail and group-call liveness asks that an archived row
      sets off wait for the walk's settle and go once. A host's
      mark_displayed still goes at once.
    • A row stored already is a duplicate by its archive id, told before its
      payload is read.
    • An OMEMO heartbeat is no longer stored in the archive (1202 → 2 a day
      on the measured backlog); the answer to a key exchange still is.

    Calls

    • calls_from_strangers drops a stranger's call before any answer;
      nothing tells them the device is online (issue 15). The installation
      setting (0 or 1) is the stranger rule for a call propose alone, so
      a host can let strangers write but not ring; unset, it follows
      messages_from_strangers, as every call did before. At 0 no
      <ringing/>, <reject/>, <Incoming> or call record, only message
      <Blocked> with reason stranger.

    Call tier / notices

    • At each <Ready> the call tier asks where the chats were read
      (XEP-0490) and tells notify <SealedRead> for a chat read elsewhere
      while it had no session, so a room read on the desktop withdraws the
      locked phone's notice.
    • A correction, a retraction, a reaction or a room's history sent again
      at a join is no new message for notify <Sealed>.

    Certificates

    • An "ignore for seven days" exception is bound to the certificate the
      user was shown; another one on that host is refused and reported as
      changed before any authentication starts (issue 16).
      ignore_certificate_errors takes the fingerprint of the last
      <CertificateUntrusted> for the host (an error when there was none),
      changed/previous_sha256 cover a live exception as they cover a pin,
      and CertificateTrust.ignore_sha256 names the certificate. An exception
      stored by an older build names none and is read as ended: the next
      connect asks again. C ABI: tacky_trust_override_t gains
      ignore_sha256_hex (connect's ignore_sha256, tls -ignore-sha256).
      ignore_certificate_errors (account and setting) takes an optional
      sha256, the event's: a dialog answered after another certificate was
      refused on the host is an error instead of an exception for one the
      user never saw. A certificate refused again after another is reported
      again, so the last event is always the one an answer binds to;
      untrust_certificate and account removal forget the refusals shown.

    Disappearing messages

    • A message's timer travels inside the OMEMO 2 envelope, so the server
      no longer sees it on a message; OMEMO 0.3 and clear chats carry it
      beside the message as before (issue 17). The timer's negotiation
      (set_timer) still goes in the clear: sealed, it is an empty message
      to every reader before this one, and a room admin's change would be
      lost there. A sealed negotiation from a peer is read, so sealing it
      can follow once readers are out there.

    Link previews

    • No page is fetched for a send that cannot carry the preview, OMEMO 0.3
      above all (issue 18): under OMEMO a send fetches its link only when its
      recipients would get an OMEMO 2 envelope, and not while the chat's keys
      are still to come.

    Internals

    • omemo: our own devices are written in the profile the peers' devices
      take where they are listed under it, so a message to an OMEMO 0.3 peer
      is one element, not two.
    • messagestore: a room's pending invite reads the account's invites, not
      the chat's every row.

    Tests

    • The catch-up against a real archive on both rigs:
      test_catchup_history.tcl (history whole, in order, once, across drops,
      resumes and restarts) and test_catchup_rooms.tcl (five rooms beside a
      1:1 backlog); twelve catch-up accounts on the rigs.
    • bench_catchup measures when the newest peer row and an open chat draw,
      and what the archive holds.
    • Issues 15 to 18: unit cases each; on the ejabberd rig a message's
      disappearing timer sealed under OMEMO 2 and its negotiation in the
      clear.

    Test results are in the tag message.

    1.2.0-rc2 — 2026-11-04

    Built on 1.2.0-rc1 (9c847b7c), with the same toolchain.

    OMEMO

    • A room send leaves out a member with no usable key and goes to the
      others (issue 12; the rule since 1.1.19, now pinned case by case): under
      blind trust a member whose keys were never verified nor refused is
      encrypted to; a refused, changed or (blind trust off) undecided key is
      skipped, and so is an undecided device of a member who verified another.
      The send fails only when no other member is reached.
    • OmemoRoomStatus.unreachable_sent (bool): true when the members in
      unreachable were left out of a send that went to the others, false
      when they stopped it. A host can now tell "not delivered to" from
      "not sent" without the event.

    Chat list / messages

    • A read or displayed mark on a captioned picture re-emits its chat row
      (issue 14): the picture and its folded caption are one row, and the
      list showed delivered until it was reloaded. No schema or wire change.

    Internals

    • bus: unsubscribe touches only the tag's own events.
    • storage: a transient store skips the fsync.

    Tests

    • Issue 13 as reported: a comment then three forwards into an OMEMO room
      leave in order (fixed since 1.1.19).
    • Issue 12's room send rule, one unit case per rule, and an integration
      case with a member without OMEMO on a real server.
    • Faster suites: cheap Argon2 for the Tcl suites (tacky-keyring's
      test-kdf feature, refused in release), OTR DSA keys from a pool, the
      native media backend worked out once per suite, in-process peers do not
      wait out the close.
    • The storage suite wants a transient store at synchronous OFF and a
      file-backed one at FULL, as the fsync skip has it.

    Test results are in the tag message.

    1.2.0-rc1 — 2026-11-04

    Built on 1.2.0-beta5 (d2df40f2), with the same toolchain. The first release candidate of 1.2: no change since 1.2.0-beta5;

    1.2.0-beta5 — 2026-11-04

    1.2.0-beta4 with a chat row's kind learnt for folders: People, Bots, Groups and Channels. The beta's full notes are on the 1.2.0-beta1 page; test results are in the tag message.

    Chat list

    • ChatEntry.bot: a 1:1 chat whose contact has the identity client/bot (from their caps or a disco#info answer; any server's bot, a gateway's among them).
    • ChatEntry.channel: a room whose disco#info carries the urn:tacky:room:1 form with kind channel.
    • Both are kept in the account's store across restarts; a change re-emits the row so the host re-filters, and a "people" folder no longer lists bots.

    Tags

    • Filter keys bot and channel (0/1), selectors beside kind: People {kind chat, bot 0}, Bots {kind chat, bot 1}, Groups {kind muc, channel 0}, Channels {kind muc, channel 1}. Older versions ignore them.

    Wire

    • A room's urn:tacky:room:1 form is read (its kind only), for joined and MUC/Sub rooms; the synced tags list carries <bot/> and <channel/>.

    1.2.0-beta4 — 2026-11-04

    1.2.0-beta3 with video added to a running call and screen sharing on desktop Linux. The beta's full notes are on the 1.2.0-beta1 page; test results are in the tag message.

    Calls

    • Video joins a running audio call: calls set_video -on 1 adds the camera on the webrtc backend (desktop, Android), the page adds it by its own offer; <Renegotiated> reports the outcome, and a refused add leaves the call on audio.
    • The peer's added video is accepted with our camera off; set_video -on 1 turns it on. <VideoEnded> when no video is left.
    • Screen share on desktop Linux: set_video -on 1 -source screen captures through the desktop portal and PipeWire, loaded at run time; calls status has screen. A screen that ends by itself mutes our video with <ScreenShare> enabled 0, and going back to a camera that is off releases the screen.
    • set_screen_share marks the added content as a screen; a peer's added screen is reported as <ScreenShare>.
    • Media ABI 1.4: capabilities renegotiate and screenCapture, video device "screen".

    Wire

    • Jingle content-add, content-accept, content-reject and content-remove for video in a running call, on the session's verified fingerprint and sealed like the descriptions.

    1.2.0-beta3 — 2026-11-03

    1.2.0-beta2 with a quieter, steadier connection on phones. The beta's full notes are on the 1.2.0-beta1 page; test results are in the tag message.

    Connection

    • Locking and unlocking the phone keeps the XMPP stream: no fresh login, no presence flood, contacts online at once after the unlock.
    • A fresh login sends its presence after the roster, carbons and catch-up only; the rest follows it. Service info is asked once per session and shared.
    • Fewer repeated requests at each connect: refused OMEMO bundles (7 days), rooms without an avatar, the push enable, gateway registration, our second presence.
    • A dead socket is noticed sooner, and while offline with a resumable session the redial comes at 5, 10, 20 s, then each minute.

    Calls and notices

    • A room read on another device withdraws the locked phone's notice of that room (XEP-0490).

    Rooms and wire

    • A room's member list lives in muc; a stale member-list reply from an earlier join is ignored.
    • Mentions use RFC 5122 URIs on the wire (nicks with spaces or # work); older libtacky stops matching such mentions.

    1.2.0-beta2 — 2026-11-02

    1.2.0-beta1 with one fix. Everything else, and the beta's notes, is on the 1.2.0-beta1 page.

    Fixes

    • Disappearing messages (XEP-0466): an album sent in the desktop's form arrives as one album again in a chat with a timer. Its bodiless announcement was taken for a timer change and dropped, so only the caption showed, as a text message.

    1.2.0-beta1 — 2026-11-02

    The first beta of the 1.2 line. New API is marked "Since 1.2.0" in schema/tacky.schema.json; hosts regenerate their bindings. Test results are in the tag message and the README.

    Encryption

    • Matrix end-to-end encryption (Olm/Megolm, cross-signing, SAS) through a key-less gateway: chats, rooms, files, edits, reactions, 1:1 calls (WIP).
    • OTR v3 secret chats with SMP; OTR announced in caps (urn:tacky:otr:0).
    • Telegram-style secret chats directly between two XMPP devices, now with files, stickers and GIFs.
    • Signal E2E: design and API stubs only.
    • Automatic Trust Management (XEP-0434/0450), opt-in; SCE time check (XEP-0420); aes-128-gcm shares (XEP-0448).

    Communities and social

    • Generic pubsub client (XEP-0060); microblogging over XEP-0472 with XEP-0470 comments and reactions; stories (XEP-0501).
    • Hats, token invite links, server-side spaces, affiliation versioning, mention notifications, account invites (XEP-0317, 0488, 0503, 0463, 0452, 0401).
    • Polls are ProcessOne's urn:fluux:poll:0 (votes as reactions); urn:tacky:poll:0 is removed and 1.1.x poll votes are not carried over.
    • WebXDC apps, Payment Required, HTTP online meetings, screen-share content category, Coin (XEP-0491, 0518, 0483, 0507, 0298).

    Connection

    • Caps 2.0 (XEP-0390), Happy Eyeballs and auth pipelining (opt-in), JSON host-meta, every XMPP URI query.
    • Offline mode is per account: account disable withdraws push and closes that account only; the installation-wide offline setting is gone.
    • setting get_many; link previews read legacy charsets (windows-1251, koi8-r…).

    Review and performance

    • Four reviews (native, encryption, Tcl/interop, performance): 2 Critical, 8 High, 16 Medium fixed, mostly in Matrix E2EE and the new feeds.
    • Catch-up SQL halved, chat list read 25.8 → 8.9 ms at 60k rows, backend start 331 → 302 ms, browser filesystem 2.58 → 1.84 MB gzipped.

    Build

    • Rust 1.99.1, MSRV 1.91, wasm on emsdk 6.0.13; HANDBOOK.md in the tree.

    Known issue

    • On ejabberd, an album sent in the desktop's form can arrive as its caption alone when many suites ran before it; under investigation.
    Downloads