Repository navigation
vector_sdk 0.7.1: private channels work — two gaps that still make a granted bot look mute #83
Description
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:
- Document it, on
BotEvent::ChannelKeyedand 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. - Give a bot a supported way to read what was back-filled — e.g.
Channel::history(limit)on the SDK.v2_backfill_channelis core-only, so today an
SDK consumer has no way to see the messages it can prove are in its own database. - 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
feat(concord): history paging, cursor sync, readiness — the headless …
feat(sdk): Channel history + Cursor + sync verbs, Ready, backfilled c…
JSKitty commentedon Aug 6, 2026
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:
BotEvent::ChannelKeyedand 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.Channel::history(limit)andhistory_before(&cursor, limit)— read what the bot provably holds, chronological, local-only. Plussync(max_events)/sync_before/sync_sincefor pulling older pages from relays under your own control, and aCursortype 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.ChannelKeyed { backfilled: usize }— "you were handed N messages you will never be called for" now rides the event. The intended pattern:One nuance you'll hit while testing:
backfilledcounts 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 withcommunities: 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
Readyis not delivered live, but it is no longer unreachable — onReady, async+historyreads it.Migrating from 0.7.1
Two breaking changes, both flagged in the changelog:
ChannelKeyedgained thebackfilledfield — add it to your pattern or match with..BotEventis now#[non_exhaustive]— add a_arm; in exchange, future event kinds stop being breaking changes