Skip to content

vector_sdk 0.7.1: private channels work — two gaps that still make a granted bot look mute #83

Closed

Description

@ngmisl

vector_sdk 0.7.1: private channels work — two gaps that still make a granted bot look mute

Follow-up to #82, which 0.7.0 closed. First: it works. Verified live today against a
Concord v2 / Armada community, in this order — enumerate (Community::channels() with
is_private() / is_readable()), admin grants access, key vends and is adopted, history
back-fills, a live message arrives on the private plane and the bot replies, and the bot
can send into the channel by id. Nothing below asks for that to change.

Two things still make a correctly granted, correctly running bot indistinguishable
from a broken one, and they compose badly. Both cost us hours today, after the upgrade.

1. Back-filled messages never reach the message handler

When the vended key is adopted, follow calls VectorCore::v2_backfill_channel(...) and
then handler.on_channel_keyed(...). The back-fill takes no handler argument, so the
messages it writes to local state are never dispatched — correct as a design (history is
not delivery), and invisible as a consequence.

From the operator's chair this is exactly the #82 symptom:

  • the bot is in the private channel and readable,
  • the client shows the question sitting in the channel,
  • the bot answers nothing, with no error,
  • and it stays that way forever, because the message it would answer is already history.

Only a new message sent after the grant reaches the handler. Nothing in the changelog
or the ChannelKeyed docs says so; the changelog says ChannelKeyed fires "so a granted
bot acts on the transition", which reads as though the transition hands you the room.

We only found it by noticing that the two messages in the channel had received_at
timestamps fifteen seconds after the key landed and a created_at from before it, while
our handler — which logs unconditionally on entry — had never been called.

Suggested fix, cheapest first:

  1. Document it, on BotEvent::ChannelKeyed and in the 0.7.0 changelog entry: "messages
    that predate the grant are back-filled to local state and are NOT delivered to your
    handler; ask the sender to re-send." One sentence would have saved the whole hunt.
  2. Give a bot a supported way to read what was back-filled — e.g.
    Channel::history(limit) on the SDK. v2_backfill_channel is core-only, so today an
    SDK consumer has no way to see the messages it can prove are in its own database.
  3. Optionally, carry the count on the event: ChannelKeyed { .., backfilled: usize }.
    "You were handed 2 messages you will never be called for" is the whole diagnosis.

2. A cold start has a window with no v2 community subscription, and no signal for it

listen() calls community::v2::realtime::refresh_subscription at startup, but phase 1
of that function waits up to ~6s for a relay to connect and then primes stream AUTH across
every community relay. Several of ours time out or refuse AUTH. V2_SUB_SET is only
assigned in phase 2, after that gauntlet.

Measured on a cold start against our real relay set (six relays, several dead or
AUTH-gating):

t after listen() subscribed_author_set()
0s … 60s empty
≤ 95s registered (a live message was delivered)
360s populated, stable

So for at least the first minute the bot holds no v2 subscription at all — it is
genuinely deaf to community traffic, not merely unreported. The only public observable is
subscribed_author_set(), and during that window it is empty, which is also what it
returns when nothing is subscribed at all. A bot sampling it early cannot tell
"still connecting" from "subscribed to nothing".

Suggested fix: emit a BotEvent when the community subscription registers (or expose
a subscription_ready() / Instant of last successful refresh_subscription). A bot that
wants to log its own health has nothing else to read.

Why the two compose

A message sent inside the startup window is not delivered live, and when it is later
back-filled it is not dispatched either. So it is never seen by the bot — not late, never.
We did not reproduce that specific loss (the messages we lost were lost to the keyless
case in §1), but it follows from the two behaviours above and is worth closing.

Environment

vector_sdk 0.7.1, vector-core 0.6.1, Rust, Linux. Concord v2 community, protocol 2,
one private channel at epoch 2 granted to the bot by an admin.

Activity

  1. JSKitty commented on Aug 6, 2026

    @JSKitty

    Both reports were accurate, and both are fixed in vector_sdk 0.8.0 / vector-core 0.7.0, on crates.io now.

    §1 — back-filled history

    All three of your suggestions shipped:

    1. Documented on BotEvent::ChannelKeyed and in the changelog, in nearly your words: messages that predate the grant are back-filled to local state and are NOT delivered to your handler.
    2. Channel::history(limit) and history_before(&cursor, limit) — read what the bot provably holds, chronological, local-only. Plus sync(max_events) / sync_before / sync_since for pulling older pages from relays under your own control, and a Cursor type with a wire-stable "<at_ms>:<message id hex>" string form you can persist anywhere to build your own sync mechanism. Cursors are compared by value, so one pointing at a since-deleted message still pages correctly.
    3. ChannelKeyed { backfilled: usize } — "you were handed N messages you will never be called for" now rides the event. The intended pattern:
    BotEvent::ChannelKeyed { community_id, channel_id, backfilled } => {
        if backfilled > 0 {
            let missed = bot.community(&community_id).channel(&channel_id).history(backfilled).await;
            // decide what deserves answering
        }
    }

    One nuance you'll hit while testing: backfilled counts messages readable under the epoch key you were vended. A fresh grant reads history from the current epoch onward; anything from before an earlier rekey stays sealed to new members by design, so your count can be smaller than what a long-standing member sees in the same channel.

    §2 — the cold-start window

    BotEvent::Ready { communities } fires when the subscription actually registers — including with communities: 0, so "subscribed to nothing" and "still connecting" are finally different states. VectorBot::subscription_ready() is the pollable twin for health checks outside the handler.

    And the window itself is largely gone: stream-auth priming no longer runs before registration. The subscription registers right after the startup catch-up sync; priming continues in the background and each AUTH-gating relay joins the stream the moment its own auth completes. One dead relay in your set no longer holds the other five hostage.

    The §-composition hazard closes with the two halves together: a message sent before Ready is not delivered live, but it is no longer unreachable — on Ready, a sync + history reads it.

    Migrating from 0.7.1

    Two breaking changes, both flagged in the changelog:

    • ChannelKeyed gained the backfilled field — add it to your pattern or match with ..
    • BotEvent is now #[non_exhaustive] — add a _ arm; in exchange, future event kinds stop being breaking changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions