Skip to content

Add experimental support for MSC4525: Paginated Sync - #20073

Open
ara4n wants to merge 18 commits into
developfrom
matthew/paginated-sync
Open

ara4n wants to merge 18 commits into
developfrom
matthew/paginated-sync

Conversation

@ara4n

@ara4n ara4n commented Aug 6, 2026

Copy link
Copy Markdown
Member

Implements MSC4525 (Paginated Sync): a dialect of Simplified Sliding Sync without lists, ranges, subscriptions, expanding timelines, connection expiry, or a client-visible error path. The client declares page_size / limit / history and the server pages it through whatever changed, most recently active rooms first, with bounded responses; rooms with updates that don't fit are reported in pending and delivered on subsequent requests.

New unstable endpoint POST /_matrix/client/unstable/org.matrix.msc4525/sync behind experimental.msc4525_enabled (default off).

The implementation is deliberately a thin layer over sliding sync:

  • PaginatedSyncHandler subclasses SlidingSyncHandler: the per-room data fetch (get_room_sync_data), extensions, connection store and notifier integration are shared.
  • The paging cursor is the existing NEVER/PREVIOUSLY/LIVE per-connection room tracking: candidates that don't fit the page are recorded unsent and picked up on the next request. Same pos token format, no schema changes.
  • The room-selection logic reuses _compute_interested_rooms_* with no lists/subscriptions to get the membership map, then sorts by recency and truncates to the page. Fairness: when more rooms are pending than fit, a quarter of the page is reserved for the rooms whose undelivered updates are oldest.
  • Two behaviour flags on the shared handler (expanded_timeline_on_limit_increase, track_room_configs) disable the expanded-timeline hack and per-room request-config persistence for this endpoint; required_state is immutable per connection, so per-connection state reduces to the sent-rooms map.
  • An unrecognised pos is treated as absent (nothing trusted from the token) rather than erroring with M_UNKNOWN_POS; num_live and the per-extension lists/rooms scoping are dropped from the wire per the MSC.

Tested by tests/rest/client/sliding_sync/test_paginated_sync.py (initial paging + drain, per-room gapping, backlog redelivery, history semantics, required_state, unknown-pos-starts-afresh, extensions without scoping); the sliding sync suites are unaffected. Also validated end-to-end against matrix-rust-sdk and Element X iOS branches of the same name (120-room account: cold start fully synced in 2 requests, overnight catch-up in one 18KB page with the connection surviving).

Code drafted by Claude at Matthew's direction.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C1H1UimuVrfVkU9Wf1phsd

ara4n added 4 commits August 6, 2026 22:31
…ing Sync

New unstable endpoint POST /_matrix/client/unstable/org.matrix.paginated_sync/sync
behind experimental.paginated_sync_enabled. The client sends page_size /
limit / history + a single top-level required_state; the server returns
the changed rooms (most recently active first, at most page_size of
them, at most limit new events each with an explicit per-room gap
beyond), plus pending (rooms that did not fit) and total_rooms.

The implementation is deliberately a thin layer over sliding sync:
PaginatedSyncHandler subclasses SlidingSyncHandler (room data fetch,
extensions, connection store and notifier integration are shared), the
servlet subclasses SlidingSyncRestServlet (room/extension serialisation
shared), and the paging cursor is the existing NEVER/PREVIOUSLY/LIVE
per-connection room tracking - candidates that don't fit the page are
recorded unsent and picked up next request. Fairness: when more rooms
are pending than fit, a quarter of the page is reserved for the rooms
whose undelivered updates are oldest. The expanded-timeline hack is
disabled for this endpoint (history is /messages' job).
Covers initial-sync paging + drain (pending/total_rooms, most-recent
first, no duplication or loss), per-room gapping (limited + prev_batch,
only the newest limit events), incremental backlog paging, history
semantics for never-sent rooms, and top-level required_state.
- No M_UNKNOWN_POS: an unrecognised pos is treated as absent (nothing
  trusted from the token); the connection starts afresh and rooms come
  down as never-sent. Clients have no error path.
- required_state is immutable per connection and always taken from the
  current request: room configs are no longer persisted or diffed
  (track_room_configs flag, off for paginated sync).
- num_live dropped from the wire (derivable: previously-sent rooms only
  receive live events, initial rooms are all-historical).
- Extension lists/rooms scoping ignored: an enabled extension applies
  to the rooms in the response (PaginatedSyncExtensionHandler).

Tests: unknown-pos-starts-afresh, extensions-without-scoping, num_live
absence; sliding sync suites unaffected (174 still green).

Also gitignore localtest/, the local validation server's scratch dir.
Endpoint /_matrix/client/unstable/org.matrix.msc4525/sync, config flag
experimental.msc4525_enabled, docs updated.
@ara4n
ara4n requested a review from a team as a code owner August 6, 2026 21:44
The override must not narrow the base method's parameter types.
ara4n added 2 commits August 7, 2026 03:08
The store only needs user/requester/conn_id from the sync config, and is
shared between sliding sync and paginated sync (MSC4525), so type its
methods with a Protocol both configs satisfy instead of SlidingSyncConfig
(fixes the mypy failure on PaginatedSyncConfig at the
get_and_clear_connection_positions call site, and drops the now-redundant
ignore on record_new_state).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds experimental MSC4525 Paginated Sync using existing Sliding Sync infrastructure.

Changes:

  • Adds the gated endpoint, request/response types, and handler.
  • Implements room paging, backlog tracking, extensions, and timeline limits.
  • Adds integration tests and a changelog entry.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
.gitignore Ignores local test artifacts.
changelog.d/20073.feature Announces MSC4525 support.
synapse/config/experimental.py Adds the feature flag.
synapse/handlers/sliding_sync/__init__.py Makes shared behavior configurable.
synapse/handlers/sliding_sync/paginated.py Implements paginated synchronization.
synapse/handlers/sliding_sync/store.py Generalizes connection configuration typing.
synapse/rest/__init__.py Registers the endpoint.
synapse/rest/client/paginated_sync.py Implements request and response handling.
synapse/server.py Exposes the new handler.
synapse/types/handlers/paginated_sync.py Defines handler types.
synapse/types/rest/client/__init__.py Defines the request model.
tests/rest/client/sliding_sync/test_paginated_sync.py Tests primary endpoint behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread synapse/handlers/sliding_sync/paginated.py
Comment thread synapse/rest/client/paginated_sync.py Outdated
Comment thread synapse/rest/client/paginated_sync.py
required_state: list[
Annotated[tuple[StrictStr, StrictStr], Field(strict=False)]
] = []
extensions: SlidingSyncBody.Extensions | None = None

@ara4n ara4n Aug 7, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately omitted for now: Synapse's MSC4186 implementation doesn't implement set_presence either, so "as MSC4186" currently means the same (non-)behaviour here. Happy to add it to both if/when sliding sync grows support. (via claude)

Comment thread synapse/config/experimental.py
Comment thread synapse/handlers/sliding_sync/paginated.py Outdated
@erikjohnston erikjohnston self-assigned this Aug 7, 2026
Comment thread synapse/rest/client/paginated_sync.py
ara4n added 10 commits August 7, 2026 13:21
With room-config tracking off, prev_room_sync_config was always None, so
the lazy-member accounting in get_room_sync_data never ran and incremental
responses were state-deltas-only: a timeline sender's membership that the
connection had never seen was silently omitted. Since required_state is
immutable per connection, treat the previous config as the current one,
which reduces _required_state_changes to exactly the lazy-member
bookkeeping.
Candidates were derived purely from the event stream, so a read receipt
(or room account data change) in an otherwise quiet room was deferred
until someone spoke in it. Rooms with undelivered receipt/account-data
changes - tracked PREVIOUSLY on the per-connection stream maps, or with
activity in the token range - now count as candidates when the relevant
extension is enabled: the room wakes into the page (usually as an empty,
filtered-out entry) and the extension delivers the data.
Add MSC4525 to the per-user ExperimentalFeature registry: the servlet is
always registered but 404s unless the feature is enabled globally
(msc4525_enabled) or for the requesting user via the admin
experimental-features API.
…omit total_rooms when unknown, conn_id caveat, drop stray .gitignore hunk

Also adds a test for the aging-lane fairness path.
Pure rename (plus the test module moving to
test_msc4525_paginated_sync.py) to make the experiment easy to grep,
remove or merge later.
…malformed pos, protect cold-start drain, fix total_rooms

- /versions now advertises org.matrix.msc4525, honouring both the global
  flag and the per-user experimental feature (as Erik requested inline).
- An unparsable pos no longer 400s: it is treated like an unrecognised
  one and the connection starts afresh (MSC4525 has no client error
  path).
- The aging lane now also reserves page slots for never-sent rooms, so
  continuous traffic in already-delivered rooms can't stall the initial
  drain indefinitely.
- total_rooms is counted before partial-state filtering so cold-start
  progress reporting isn't understated.
Removing M_UNKNOWN_POS is about well-formed positions the server no
longer recognises; a malformed token is a plain client error like
everywhere else. Reverts part of 20feb1e and keeps a test pinning
the 400.
complement main has no tests/msc4429 (matrix-org/complement#849 is
unmerged), so since 19556 merged, every complement CI run on develop
build-fails on the missing package (obscured by a gotestfmt panic on the
mid-stream module downloads). Drop the package here so this PR's CI is
meaningful; restore the line once complement#849 lands.
@ara4n

ara4n commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Heads-up: develop's complement CI is currently broken for every PR - #19556 added ./tests/msc4429 to scripts-dev/complement.sh but matrix-org/complement#849 (which adds that package) hasn't merged, so the package build-fails (directory not found, obscured by a gotestfmt panic on the mid-stream go: downloading noise from complement's new moby deps). Both develop runs since fail identically.

To keep this PR's CI meaningful I've merged develop and added fe471df, an XXX REVERT BEFORE MERGE commit dropping that one package line; it should be dropped once complement#849 lands (or develop is otherwise fixed). (via claude)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants