feat(updates): apply affectedMessages/affectedHistory pts#1782
Conversation
Methods like messages.readHistory, messages.deleteMessages and channels.deleteMessages return messages.affectedMessages / messages.affectedHistory, which carry a pts increment the client must apply. These results are not tg.UpdatesClass, so they bypassed the update manager entirely: the server pts advanced while the local pts stayed behind, making the next genuine update (e.g. an edit) look like a gap that was postponed until the next pts-changing update. Fixes #1382. - Add Manager.HandleAffected(ctx, channelID, pts, ptsCount): applies the increment to the common sequence, or to a tracked channel's sequence, via a non-dispatchable affectedPts marker that advances pts without fabricating a user-visible update. pts==0 is ignored (it would reset the sequence to 0). - Add hook.AffectedHook middleware that wires it from RPC results automatically, routing by request (InputChannel / InputPeerChannel -> channel; otherwise common). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1782 +/- ##
==========================================
- Coverage 71.32% 71.15% -0.18%
==========================================
Files 504 505 +1
Lines 23625 23748 +123
==========================================
+ Hits 16851 16897 +46
- Misses 5542 5612 +70
- Partials 1232 1239 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Add hook.AffectedHook alongside the existing update middleware so the updates/userbot examples keep local pts in sync after self-initiated reads and deletes. See #1382. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a pts desynchronization bug where self-initiated RPCs returning messages.affectedMessages / messages.affectedHistory could advance server pts without advancing the local updates manager pts, causing subsequent pts-based updates (e.g., edits) to be buffered until another pts update arrives (issue #1382).
Changes:
- Add an internal “non-dispatchable” pts marker (
affectedPts) and plumbing (Manager.HandleAffected+ state queues) to apply affected pts increments through the normal sequence handling without fabricating user-visible updates. - Add
hook.AffectedHookmiddleware to automatically detectmessages.affected*RPC results and route them to the correct pts sequence (common vs channel). - Add focused tests for gap-filling behavior and middleware detection/routing, plus update examples to show middleware usage.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| telegram/updates/update.go | Introduces affectedPts marker type used to advance pts without dispatch. |
| telegram/updates/state.go | Adds affected queue + handling in internal state loop; applies affected pts to common/channel sequences. |
| telegram/updates/state_channel.go | Adds channel-side affected handling and ensures markers don’t dispatch. |
| telegram/updates/state_apply.go | Skips dispatch on marker-only batches while still persisting pts. |
| telegram/updates/state_affected_test.go | Adds tests reproducing #1382 and validating affected-pts semantics. |
| telegram/updates/manager.go | Exposes Manager.HandleAffected API (no-op before Run). |
| telegram/updates/hook/affected.go | Adds middleware to extract affected pts from RPC results and route by request. |
| telegram/updates/hook/affected_test.go | Tests middleware detection and routing behavior. |
| examples/userbot/main.go | Wires AffectedHook into userbot example middleware chain. |
| examples/updates/main.go | Wires AffectedHook into updates example middleware chain. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func channelIDFromRequest(input bin.Encoder) int64 { | ||
| if r, ok := input.(interface { | ||
| GetChannel() tg.InputChannelClass | ||
| }); ok { | ||
| if ch, ok := r.GetChannel().(*tg.InputChannel); ok { | ||
| return ch.ChannelID | ||
| } | ||
| return 0 | ||
| } | ||
| if r, ok := input.(interface { | ||
| GetPeer() tg.InputPeerClass | ||
| }); ok { | ||
| if p, ok := r.GetPeer().(*tg.InputPeerChannel); ok { | ||
| return p.ChannelID | ||
| } | ||
| } | ||
| return 0 | ||
| } |
channelIDFromRequest only recognized *tg.InputChannel and *tg.InputPeerChannel, so *tg.InputChannelFromMessage and *tg.InputPeerChannelFromMessage (both carry a ChannelID) fell through to the common sequence. That misapplied a channel pts to the common sequence and could desync update processing. Route via the shared GetChannelID() accessor (implemented by all four channel inputs), and skip instead of defaulting to common when a channels.* request carries no resolvable channel (e.g. InputChannelEmpty). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover updhook.AffectedHook from gotd/td#1782: add it to the wiring snippet and explain why self-initiated reads/deletes (messages.affectedMessages / affectedHistory) need it to keep local pts in sync and avoid postponed updates (issue #1382). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #1382.
Problem
A user account stops receiving an update (e.g. an edit) until the next pts-changing update arrives. Reproduction (from #1382): receive a message, read it, then have the other side edit it — the
UpdateEditMessageis postponed.Root cause: methods such as
messages.readHistory,messages.deleteMessages,channels.deleteMessages,messages.readMentions,messages.deleteHistoryreturnmessages.affectedMessages/messages.affectedHistory, which carry a pts increment the client must apply. These results are nottg.UpdatesClass, so they never reach the updates manager —updhook.UpdateHookonly forwards*tg.UpdatesBoxresults. The server pts advances while the local pts stays behind, so the next genuine pts update looks like a gap and is buffered:The reporter's own
messages.readHistorycall (insideOnNewMessage) is what creates the missing pts. This matches TDLib, which applies the affected pts in each query's result handler.Fix
Manager.HandleAffected(ctx, channelID, pts, ptsCount)— applies the increment to the common sequence, or to an already-tracked channel's sequence. It feeds a non-dispatchableaffectedPtsmarker through the normalsequenceBox, so the pts advances (and fills any open gap, releasing postponed updates) without fabricating a user-visible update.applyPtsskips the marker; an empty batch dispatches nothing but still persists pts.pts == 0is ignored — feeding 0 to asequenceBoxwould reset its state to 0 and desync everything.getChannelDifferencereconciles later).hook.AffectedHook(handler)middleware — wires it from RPC results automatically. Routing by request: anInputChannel(channels.*) orInputPeerChannelpeer (messages.*targeting a channel) → that channel; everything else → the common sequence.Opt-in alongside
UpdateHook:Tests
TestHandleAffectedFillsGapreproduces Bug: update postponed and handled only with next update #1382: an edit postponed behind a missing read-pts is delivered (in order, exactly once) after the affected pts is applied.TestHandleAffectedAdvancesPts/ZeroIgnored/UntrackedChannelIgnoredcover the marker, the pts==0 guard, and channel gating.TestAffectedHookcovers result detection and common-vs-channel routing (by peer, byInputChannel, no-peer), plus RPC-error short-circuit.All
./telegram/...tests pass, including-raceontelegram/updates.Notes
Run.telegram/updatesbut do not overlap.🤖 Generated with Claude Code