From 3738e568f3f4c8e48eed85860f7ccbf7246fe9cf Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Mon, 15 Jun 2026 13:27:53 +0100 Subject: [PATCH 1/2] connector: recover never-bridged messages in forward backfill Forward backfill pruned the fetched batch purely by timestamp, dropping every message older than the portal's latest bridged message on the assumption it was already bridged. Meta can skip threads in a reconnect snapshot while advancing the sync cursor past their messages, so a later ChatResync forward backfill fetches those missed messages and then discards them here, leaving permanent mid-timeline gaps (PLAT-36990). Before dropping a pre-anchor message, check whether it actually exists in the bridge database and recover it if it does not. The lookup only runs for the pre-anchor slice of a forward backfill batch and fails closed (drops as before) on DB errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/connector/backfill.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 6a6a4255..942024d0 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -321,7 +321,29 @@ func (m *MetaClient) wrapBackfillEvents(ctx context.Context, portal *bridgev2.Po if anchor != nil { if forward { upsert.Messages = slices.DeleteFunc(upsert.Messages, func(message *table.WrappedMessage) bool { - return message.TimestampMs <= anchor.Timestamp.UnixMilli() + if message.TimestampMs > anchor.Timestamp.UnixMilli() { + return false + } + // This message is older than the portal's latest bridged message, so it would + // normally be assumed to already exist. However, Meta can skip threads in a + // reconnect snapshot while advancing the sync cursor past their messages (see + // PLAT-36990), which leaves permanent mid-timeline gaps. Only drop the message + // if it actually exists in the bridge database, otherwise recover it. + existing, err := m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(message.MessageId)) + if err != nil { + zerolog.Ctx(ctx).Err(err). + Str("message_id", message.MessageId). + Msg("Failed to check message existence during forward backfill dedup, dropping") + return true + } + if existing == nil { + zerolog.Ctx(ctx).Warn(). + Str("message_id", message.MessageId). + Int64("timestamp_ms", message.TimestampMs). + Msg("Recovering pre-anchor message missing from database in forward backfill") + return false + } + return true }) } else { upsert.Messages = slices.DeleteFunc(upsert.Messages, func(message *table.WrappedMessage) bool { From 5a268e370e88f739f8fbcf77ac09ff94edf3e5fd Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Mon, 15 Jun 2026 13:27:53 +0100 Subject: [PATCH 2/2] messagix: default sync params when page config omits them messenger-lite sessions don't include LSPlatformMessengerSyncParams, so getSyncParams returned an empty string for the Contact sync channel (database 2). Meta's server then failed every connect with "Invalid argument supplied for foreach()", the database never obtained a cursor, and contact sync was permanently broken (PLAT-36990). Fall back to {"locale":"en_US"} when the config value is empty, and surface the previously-unhandled LSHandleSyncFailure at error level with the database ID so this class of failure is visible. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/messagix/syncManager.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/messagix/syncManager.go b/pkg/messagix/syncManager.go index e48ec4ef..428e8890 100644 --- a/pkg/messagix/syncManager.go +++ b/pkg/messagix/syncManager.go @@ -125,8 +125,12 @@ func (sm *SyncManager) SyncSocketData(ctx context.Context, databaseID int64, db resp.Finish() if len(resp.Table.LSHandleSyncFailure) > 0 { - // TODO handle these somehow? - sm.client.Logger.Warn(). + // These failures mean the server-side sync for the database errored and no + // cursor was obtained, so the database stays unsynced until the next attempt. + // A common cause is empty sync params (handled in getSyncParams, see + // PLAT-36990); log at error level with the database ID so it's visible. + sm.client.Logger.Error(). + Int64("database_id", databaseID). Any("sync_failures", resp.Table.LSHandleSyncFailure). Msg("Sync failures found") } @@ -236,18 +240,30 @@ func (sm *SyncManager) UpdateDatabaseSyncParams(dbs []*socket.QueryMetadata) err var dbID7Params = `{"mnet_rank_types":[44]}` +// defaultLocaleSyncParams is the fallback sync params payload used when the page +// config doesn't provide LSPlatformMessengerSyncParams. messenger-lite sessions +// don't include these, so an empty string would make Meta's server fail with +// "Invalid argument supplied for foreach()" (see PLAT-36990), permanently +// breaking the affected sync database (e.g. Contact sync on database 2). +var defaultLocaleSyncParams = `{"locale":"en_US"}` + func (sm *SyncManager) getSyncParams(dbID int64, ch socket.SyncChannel) *string { if dbID == 7 { return &dbID7Params } + var params *string switch ch { case socket.MailBox: - return &sm.syncParams.Mailbox + params = &sm.syncParams.Mailbox case socket.Contact: - return &sm.syncParams.Contact + params = &sm.syncParams.Contact default: - return &sm.syncParams.E2Ee + params = &sm.syncParams.E2Ee + } + if params == nil || *params == "" { + return &defaultLocaleSyncParams } + return params } func (sm *SyncManager) GetCursor(db int64) string {