element-hq / element-hq/synapse
Simplified Sliding Sync drops a departed user's own membership from `required_state` after a history purge
- Dominant language
- Python
- Stars
- 4.6k
- Forks
- 600
- Avg merge
- 5d 22h
- Merged PRs (30d)
- 51
Description
*As discussed in [`#synapse-dev:matrix.org`](https://matrix.to/#/!i5D5LLct_DYG-4hQprLzrxdbZ580U9UB6AEgFnk6rZQ/$SWE8eJCT9uEvSmAzURDMuwWCEWwpHARu7ZJmSWkCM5w?via=element.io&via=matrix.org&via=beeper.com),*
### Description
After `purge_history` runs over the point in a room's timeline where a user left / was kicked / was banned, Simplified Sliding Sync (MSC4186 / `org.matrix.simplified_msc3575`) stops returning that user's own `m.room.member` event in the room's `required_state` even though the membership is still durably recorded in current-state tables. The room is still returned in the response, just with no `$ME` membership event.
### Steps to reproduce
I created a Complement test which reproduces the problem: https://github.com/codyfinn/synapse/blob/c117852b8aa27ec01996c8cc28690cc268a22203/complement/tests/sliding_sync_purged_membership_regression_test.go
Manual steps:
- Register `@alice` (room admin) and `@bob`.
- As `@alice`, create a public room: `POST /_matrix/client/v3/createRoom {"preset": "public_chat"}` → `!room:hs1`.
- As `@bob`, join: `POST /_matrix/client/v3/join/!room:hs1`. Wait for the join to sync.
- As `@alice`, send a few `m.room.message` events so there is joined-era history before the departure (gives the purge something to outlier behind).
- Depart `@bob` — any of the three reproduces:
- kick: `POST /_matrix/client/v3/rooms/!room:hs1/kick {"user_id": "@bob:hs1"}`
- ban: `POST /_matrix/client/v3/rooms/!room:hs1/ban {"user_id": "@bob:hs1"}`
- leave: `POST /_matrix/client/v3/rooms/!room:hs1/leave` (as `@bob`)
- As `@alice`, send one more `m.room.message` *after* the departure and record its `event_id` (`$boundary`). This is the purge boundary — everything topologically before it (including bob's `m.room.member` event) will be purged.
- As admin, purge history up to the boundary:
- `POST /_synapse/admin/v1/purge_history/!room:hs1` with `{"purge_up_to_event_id": "$boundary", "delete_local_events": true}`
- poll `GET /_synapse/admin/v1/purge_history_status/{purge_id}` until `"status": "complete"`
- As `@bob`, issue a Simplified Sliding Sync request asking for his own membership:
```jsonc
POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync
{
"lists": { "all": { "ranges": [[0, 99]],
"required_state": [["m.room.member", "@bob:hs1"]], "timeline_limit": 1 } },
"room_subscriptions": { "!room:hs1": {
"required_state": [["m.room.member", "@bob:hs1"]], "timeline_limit": 1 } }
}
```
### Homeserver
A personal homeserver
### Synapse Version
1.155.0
### Installation Method
Docker (matrixdotorg/synapse)
### Database
PostgreSQL single server. Never ran SQLite. Never restored from a backup
### Workers
Multiple workers
### Platform
Reproduced on Complement Synapse image — Debian trixie, Python 3.13, running under Docker on macOS
Running in a Kubernetes deployment
### Configuration
_No response_
### Relevant log output
I can't share server logs, but it is reproducible with a Complement test found here: https://github.com/codyfinn/synapse/blob/c117852b8aa27ec01996c8cc28690cc268a22203/complement/tests/sliding_sync_purged_membership_regression_test.go
### Anything else that would be useful to know?
_No response_
Contributor guide
Research direction
Start with complement/tests/sliding_sync_purged_membership_regression_test.go and run the reproduction against Synapse. Trace the Simplified Sliding Sync handling for the requested m.room.member state after the purge scenario. Done means the room response includes the departed user's own membership event in required_state for kick, ban, and leave cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100