diff --git a/crates/rustyclaw-core/src/messengers/matrix_cli.rs b/crates/rustyclaw-core/src/messengers/matrix_cli.rs index 49c8197..32ce610 100644 --- a/crates/rustyclaw-core/src/messengers/matrix_cli.rs +++ b/crates/rustyclaw-core/src/messengers/matrix_cli.rs @@ -392,9 +392,15 @@ impl MatrixCliMessenger { } let mut messages = Vec::new(); + + // Track whether any allowed rooms appeared in this sync. + // We only advance the sync token if allowed rooms were present, + // to avoid skipping messages when sync returns only non-allowed room events. + let mut allowed_rooms_in_sync = false; // Get current DM rooms for filtering let dm_rooms = self.dm_rooms.lock().await.clone(); + let has_room_filters = !self.allowed_chats.is_empty() || !dm_rooms.is_empty(); eprintln!("DEBUG: sync - allowed_chats: {:?}, dm_rooms: {:?}", self.allowed_chats, dm_rooms); if let Some(rooms) = sync_response.rooms { @@ -404,13 +410,16 @@ impl MatrixCliMessenger { // Check if this room is allowed let in_allowed_chats = self.allowed_chats.contains(&room_id); let in_dm_rooms = dm_rooms.contains(&room_id); + let is_allowed_room = in_allowed_chats || in_dm_rooms; // If we have an allowlist OR dm_rooms, only process rooms in one of them - if !self.allowed_chats.is_empty() || !dm_rooms.is_empty() { - if !in_allowed_chats && !in_dm_rooms { + if has_room_filters { + if !is_allowed_room { eprintln!("DEBUG: skipping room {} (not in allowed lists)", room_id); continue; } + // An allowed room appeared in this sync + allowed_rooms_in_sync = true; } eprintln!("DEBUG: processing room {}", room_id); @@ -450,12 +459,21 @@ impl MatrixCliMessenger { } } - // Only update sync token AFTER successful message extraction. - // This prevents losing messages if extraction fails partway through. - { + // Only advance sync token if: + // 1. We extracted messages, OR + // 2. Allowed rooms appeared in sync (even with no messages = caught up), OR + // 3. No room filters configured (process everything) + // + // This prevents the token from advancing when sync only contains + // events for non-allowed rooms, which would cause us to miss messages. + let should_advance_token = !messages.is_empty() || allowed_rooms_in_sync || !has_room_filters; + + if should_advance_token { let mut token = self.sync_token.lock().await; *token = Some(next_batch.clone()); self.save_sync_token(&next_batch); + } else { + eprintln!("DEBUG: sync - NOT advancing token (no allowed rooms in response)"); } Ok(messages) diff --git a/docs/matrix-sync-fix.md b/docs/matrix-sync-fix.md new file mode 100644 index 0000000..a4dce91 --- /dev/null +++ b/docs/matrix-sync-fix.md @@ -0,0 +1,67 @@ +# Matrix Sync Token Bug Fix Plan + +## Problem +The sync token advances even when no messages are extracted from allowed rooms. This causes messages to be missed when: +1. Sync returns events only for non-allowed rooms +2. Sync returns non-message events (typing, read receipts) for allowed rooms +3. Messages arrive during concurrent processing + +## Root Cause +```rust +// Currently: Always save token after sync, even if no messages extracted +self.save_sync_token(&next_batch); +``` + +## Solution Options + +### Option 1: Only advance token when messages extracted (WRONG) +This would cause infinite re-processing of non-message events. + +### Option 2: Track "seen" event IDs (Complex) +Store event IDs of processed messages, skip duplicates. Memory grows over time. + +### Option 3: Per-room sync tokens (Best) +Matrix supports per-room `prev_batch` tokens. Track last seen event per room. + +### Option 4: Check if allowed rooms had ANY events (Simple fix) +Only advance token if allowed rooms were present in sync response, regardless of whether they had messages. + +## Recommended Fix: Option 4 + +```rust +// Track whether any allowed rooms appeared in this sync +let mut allowed_rooms_in_sync = false; + +if let Some(rooms) = sync_response.rooms { + if let Some(joined_rooms) = rooms.join { + for (room_id, room_data) in joined_rooms { + let in_allowed = self.allowed_chats.contains(&room_id) || dm_rooms.contains(&room_id); + if in_allowed { + allowed_rooms_in_sync = true; + // ... process messages as before + } + } + } +} + +// Only advance token if: +// 1. We extracted messages, OR +// 2. Allowed rooms were in sync (even with no messages = they're caught up) +// 3. No allowed rooms configured (process everything) +if !messages.is_empty() || allowed_rooms_in_sync || (self.allowed_chats.is_empty() && dm_rooms.is_empty()) { + self.save_sync_token(&next_batch); +} +``` + +## Why This Works +- If allowed rooms appear in sync with no messages → token advances (caught up) +- If sync only has events for non-allowed rooms → token does NOT advance +- Next sync will include the same batch + any new events +- Eventually the allowed room will appear and token advances + +## Edge Case: Stuck Token +If we never get events for allowed rooms, token never advances. This is fine — we'll get them eventually when someone sends a message. + +## Implementation +File: `crates/rustyclaw-core/src/messengers/matrix_cli.rs` +Lines: ~455-460 (save_sync_token section)