@@ -103,6 +103,7 @@ open class SessionRepository(
103103 val fresh = runCatchingCancellable { api.listMessages(sessionId) }
104104 .onFailure { Log.w("SessionRepository", "Re-seed message load failed for $sessionId; relying on SSE: ${safeExceptionSummary(it)}") }
105105 .getOrDefault(emptyList())
106+ var seedChanged = false
106107 lock.withLock {
107108 if (generation == seedGeneration) {
108109 // Merge without pruning: SSE events arriving during the
@@ -111,10 +112,10 @@ open class SessionRepository(
111112 // Stale messages from deletions during the disconnect
112113 // gap are eventually removed via MessageRemoved events
113114 // or on the next app restart.
114− store.seed(fresh, prune = false)
115+ seedChanged = store.seed(fresh, prune = false)
115116 }
116117 }
117− publish()
118+ if (seedChanged) publish()
118119 }
119120 hasConnectedBefore = true
120121 }
@@ -159,7 +160,8 @@ internal class MessageStore {
159160
160161 fun snapshot(): List<MessageWithParts> = messages.values.toList()
161162
162− fun seed(initial: List<MessageWithParts>, prune: Boolean = false) {
163+ fun seed(initial: List<MessageWithParts>, prune: Boolean = false): Boolean {
164+ var changed = false
163165 // On re-seed (after SSE reconnect), remove messages that are no longer in the
164166 // server snapshot (e.g. deleted during the disconnection gap). This keeps the
165167 // in-memory state in sync with the authoritative REST snapshot rather than
@@ -167,13 +169,16 @@ internal class MessageStore {
167169 // racing with just-arrived SSE events whose messages may not yet be in REST.
168170 if (prune) {
169171 val incomingIds = initial.mapTo(mutableSetOf()) { it.info.id }
172+ val before = messages.size
170173 messages.keys.retainAll(incomingIds)
174+ if (messages.size != before) changed = true
171175 }
172176
173177 for (m in initial) {
174178 val existing = messages[m.info.id]
175179 if (existing == null) {
176180 messages[m.info.id] = m
181+ changed = true
177182 } else {
178183 // A part streamed in between subscribe and this initial load may already
179184 // have populated this message (see observeMessages). Merge instead of
@@ -187,10 +192,15 @@ internal class MessageStore {
187192 if (p.id !in snapshotIds) ordered.add(p)
188193 }
189194 val info = if (existing.info is UnknownMessage) m.info else existing.info
190− messages[m.info.id] = MessageWithParts(info = info, parts = ordered)
195+ val merged = MessageWithParts(info = info, parts = ordered)
196+ if (merged != existing) {
197+ messages[m.info.id] = merged
198+ changed = true
199+ }
191200 }
192201 }
193− evictOldMessages()
202+ if (evictOldMessages()) changed = true
203+ return changed
194204 }
195205
196206 /** Returns true if the state changed (and a new snapshot should be published). */
@@ -280,11 +290,15 @@ internal class MessageStore {
280290 return true
281291 }
282292
283− /** Evict the oldest messages when the store exceeds [maxMessages], keeping memory bounded. */
284− private fun evictOldMessages() {
293+ /** Evict the oldest messages when the store exceeds [maxMessages], keeping memory bounded.
294+ * Returns true if any messages were evicted. */
295+ private fun evictOldMessages(): Boolean {
296+ var evicted = false
285297 while (messages.size > maxMessages) {
286298 val oldestKey = messages.keys.iterator().next()
287299 messages.remove(oldestKey)
300+ evicted = true
288301 }
302+ return evicted
289303 }
290304}