Repository navigation
fix: deliver a Discord member viewport chosen while offline after the session resumes - #50
Merged
Merged
Conversation
Owner
Author
|
@vbmithr this one fixes the queue-loss case you reproduced in #42. What changed:
Tests:
Not covered: a guild unsubscribe that was already written but lost with the socket isn't sent again. Only the current subscription is repeated on resume. Fixing that would mean tracking per guild what Discord actually received. To try it: git fetch origin pull/50/head:pr-50 && git checkout pr-50
cargo test -p chatd --test discord member_viewport_changed_while_offline
cargo test -p chatd adapters::discord::gateway
cargo test -p chatd # full daemon suiteIf you still have your local audit test, running it against this branch would be a useful second check. So would a real suspend/resume with the member pane open. |
… session resumes A new gateway connection cleared the whole send queue, so a subscription queued while the socket was down was lost while Watched.sent already named it, and nothing said it again on the resumed session. A resumed connection now keeps every subscription change Discord may not have heard: those never written, and those written since the last heartbeat Discord answered, since a frame written as the socket died may never have arrived. They go as one frame once RESUMED comes. Commands are still dropped, and a new session still starts empty. Every connection also starts a minute of its own, as Discord counts the allowance per connection, so the old connection's sends, or a clock that stood still through a suspend, never hold the kept frame back. Fixes #42 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
itz4blitz
force-pushed
the
fix/discord-resume-member-subscription
branch
from
October 20, 2026 13:32
2bdc2e4 to
c7fa3cb
Compare
…lippy passes The `while beat.ready` loop added with the send pacing never changes its condition and ends only on the queue's `break`, which clippy denies as `while_immutable_condition`. An `if` around a `loop` sends the same frames and still lets the queue go before each write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cause
members.rs::subscriberecordsWatched.sentwhen a subscription is queued, not when it is written.gateway.rs::runcleared the whole outbox on every new connection, so a subscription queued while offline, or still unwritten when the socket dropped, was thrown away. Becausesentalready named it, later identical Watches were deduplicated, and nothing on the resumed session said it again. The session resumed with Discord still holding the old viewport, and the member pane stayed pending.Fix
Rebased onto main, on top of #52's pacing (the send queue owns the rolling minute) and #51's watch/place split. The whole change is now in
gateway.rs'sSendQueue:SendQueue::reconnected(resuming): a connection that resumes the session keeps every subscription change Discord may not have heard, merged into one frame: the ones never written, and the ones written since the last heartbeat Discord answered. A frame written as the socket died may never have arrived; Discord reads in order, so a heartbeat ack means everything written before that heartbeat was heard. Commands are dropped as before, and a connection that identifies a new session still starts empty.sentminute and per-guildsubscribedtimes are not carried over. Before this, a resume after 80 subscription frames held the kept snapshot back 59 s, after 100 commands the next command 59 s, and after an 8-frame scroll the snapshot 733 ms. After a suspend it would be worse, sinceInstantdoes not advance during sleep on macOS and Linux.beat()and op 11 callsanswered()to track what has been heard. They only move entries between two small maps keyed by guild, newest state per guild.Changes from the first version of this PR:
RESUMEDno longer calls a separateresubscribe(). That re-sent only the guild on screen, so if{A: off, B: on}was written as the socket died and never arrived, nothing re-sent A's unsubscribe, and A stayed subscribed for the session. The queue now re-sends what was not heard, including that unsubscribe.members.rsis unchanged from main.pop(now)(the first version calledpop()and no longer built against main).Nothing changes for READY or identify.
Validation
member_viewport_changed_while_offline_is_delivered_after_resumeincrates/chatd/tests/discord.rs, not ignored. It subscribes to 4101, drops the socket, sends a Watch for 4105 while offline, waits for the resume, then asserts that an op 37 for 4105 arrives after the op 6 resume frame and that the member cache fills from a SYNC. It fails when every connection clears the queue (main's behaviour) and passes with this change. Run 6 times with fix: subscribe the Discord member viewport a Watch chose before READY #51'smember_viewport_selected_before_ready_is_subscribed_after_connect: 6/6 passed.a_resumed_connection_keeps_unheard_subscriptions_and_a_new_session_does_not. Changes heard before an answered heartbeat are not sent again. Changes written on either side of an unanswered heartbeat, plus the unwritten ones, go as one frame with the newest state per guild, including an earlier guild's unsubscribe. Commands are not carried. A new session keeps nothing.a_resumed_connection_starts_its_own_minute, on fix: pace Discord member subscriptions so a scroll cannot spend the Gateway budget #52's test clock. The old connection spends its whole allowance (80 subscription frames, then commands up to 100), and the snapshot waiting behind them is due more than 30 s out. Afterreconnected(true)that snapshot goes at once with nothing else, commands are not carried, a new command goes at once, and the new connection has its own burst of 8.reconnected(only clearing commands) and pass with this one.cargo test -p chatd --lib gateway: 6 passed.cargo test -p chatd --test discord: 32 passed.cargo test -p chatd: 294 passed, 0 failed, 0 ignored (138 lib unit tests, plus blobs 14, calls 10, discord 32, e2e 25, messages 5, playback 4, polish 12, signal 10, slack 15, teams 7, telegram 17, upload 5).cargo clippy -p chatd --all-targets: clean. A second commit turns thewhile beat.readyloop ingateway.rs::run(from fix: pace Discord member subscriptions so a scroll cannot spend the Gateway budget #52) into anifaround aloop, because clippy denied it aswhile_immutable_conditionand stopped a plain clippy run on main. It sends the same frames, and the queue's lock is still released before each write.Fixes #42
🤖 Generated with Claude Code