Repository navigation
Show a placeholder for messages this version can't process, and retain them - #808
Merged
mpretty-cyro merged 6 commits intoOct 8, 2026
Merged
mpretty-cyro merged 6 commits into
mpretty-cyro merged 6 commits into
Conversation
…n them Messages the client positively identifies as unprocessable are now kept instead of dropped. Random decryption failures still produce nothing. - One-to-one data starting 0x00 is a newer wire format. It's retained without decrypting and raises an account-level "update" banner, with no bubble, since its sender can't be known. - Decrypted content with a top-level field above 18 and nothing usable gets a placeholder bubble in an existing conversation (never a new one), incoming or outgoing for our own syncs and group messages. - Raw swarm data is kept in `unsupported_message` (same schema on all clients, for the future 2.0 import), capped at 256 MB and expired with disappearing settings. - A startup job replays retained messages after an update and replaces placeholders in place. The bubble and banner strings are hard-coded English and need adding to Crowdin.
mpretty-cyro
commented
Oct 7, 2026
- Start the placeholder variants at 100 to leave room for more standard variants. - Enforce the retention limits from the reprocess job (launch and becoming active), not inside the disappearing-messages delete, and catch its failures. - A failed replay keeps the retained message (stamped and logged) for a later version instead of dropping it. - Only add a placeholder to a visible conversation, and never un-hide one. - Record the sender and sent timestamp, so an unsend removes a retained message that has no placeholder rather than letting a later replay restore it. - Add the new migration and table to DatabaseSpec.
The notification extension notified for a hidden conversation, where the main app adds no placeholder, so the notification pointed at nothing. Also covers the replay savepoint rollback in UnsupportedMessageSpec.
- Count each retained row as its data plus 256 bytes against the 256 MB budget, and cap newer-format rows at 10,000, so a flood of tiny rows can't outgrow the limit. - Keep a running total in `unsupported_message_stats` (maintained by triggers) instead of summing the table on every insert. - Stamp newer-format rows as attempted in one update without loading them, and page the rest; replay decides by the stored kind, never the first byte. - Name the protobuf wire types in the field scan.
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.
Contributor checklist
Description
Shows a placeholder for messages this client positively identifies as something it can't process, and retains the raw data so a newer version (or the future Session 2.0 database import) can process it. Matching changes: Android session-foundation/session-android#2232, Desktop session-foundation/session-desktop#2025.
The distinction is the feature: only messages we positively identify get anything. A message that simply fails to decrypt still produces nothing, because it can't be attributed to anyone (spam, corruption or an attacker), and a bubble claims someone really sent you something.
What's detected
0x00. v1 data is protobuf, which can never start with a zero byte. It's checked before decryption, in the one-to-one namespace only. Its sender is inside the encryption and the prefix is unauthenticated, so it gets no bubble, only the banner, and its bytes are retained invisibly.Contentholds a top-level field numbered above 18 and nothing this client can use. Every number up to 18 has been assigned at some point, including 2.0'smsgId = 18.SNProtoContentdoesn't expose unknown fields, so a small hand-written tag scan runs over the decrypted bytes. One-to-one and groups v2 only; communities are excluded.What the user sees
dataMessage.syncTarget) and in groups. It's only added to a conversation that already exists and is visible: never a new conversation or message request, and it never un-hides one. Placeholders only offer "delete for me". The notification extension stays silent for newer-format messages and shows the generic "new message" notification only when a placeholder will be shown.InfoBannerat the top of Home, with a ✕ to dismiss. It's raised by a newer-format message ("Some messages can't be shown on this device…") or by an unknown type from our own other device that can't be placed ("One of your other devices is using a newer version…"). After dismissal it reappears only for a trigger at least 7 days later.Storage and replay
unsupported_message, migration_054. Columns are identical across iOS, Android and Desktop so the future import reads one shape.senderandsent_timestamp_msare stored for unknown types.Needs doing before merge
FIXME).Testing
UnsupportedMessageSpec24/24. It covers the tag scan, detection rules, placement, hidden conversations, unsend, deletion, expiry, banner state, and replay: replaced, still unsupported, parse failure, and savepoint rollback.NotificationsManagerSpecandDatabaseSpecwere updated for the new type, migration and table, and pass.SwarmPollerSpec"config message fails to merge on the normal (non-synchronous) poll path" crashes inJobRunner.init. This change doesn't touch that path.