@@ -102,7 +102,10 @@ open class SessionRepository(
102102
103103 fun schedule(snapshot: List<MessageWithParts>) {
104104 val cache = messageCache ?: return
105− if (snapshot.isEmpty()) return
105+ // Don't early-return on an empty snapshot: MessageCacheStore.save deletes the file
106+ // for an empty list, which is exactly what we want when a conversation becomes empty
107+ // via MessageRemoved events. Skipping here would leave stale messages cached on disk
108+ // until an explicit remove() on session deletion.
106109 latest = snapshot
107110 val now = System.currentTimeMillis()
108111 pending?.cancel()
@@ -179,7 +182,11 @@ open class SessionRepository(
179182 if (cached.isNotEmpty()) {
180183 val changed = lock.withLock {
181184 if (seedGeneration == 0) {
182− cacheSeededIds = cached.mapTo(mutableSetOf()) { it.info.id }
185+ // Store the synthetic key for UnknownMessages (which have empty ids) so
186+ // pruneStaleCacheSeeded's messages.remove(id) hits the actual map key
187+ // the store used ("unknown-c<created>" / "unknown-h<hash>") instead of
188+ // the empty id that never matches and leaves ghost entries un-pruned.
189+ cacheSeededIds = cached.mapTo(mutableSetOf()) { store.syntheticKeyFor(it) }
183190 store.seed(cached, clearPivot = false)
184191 } else false
185192 }
@@ -522,7 +529,7 @@ internal class MessageStore {
522529 * synthetic key ("unknown-c<created>" or "unknown-h<hash>"); derive the same key here so
523530 * a REST seed merges onto the existing holder instead of missing it and inserting a
524531 * duplicate under key "". */
525− private fun syntheticKeyFor(m: MessageWithParts): String =
532+ internal fun syntheticKeyFor(m: MessageWithParts): String =
526533 if (m.info.id.isEmpty() && m.info is UnknownMessage) {
527534 m.info.time?.created?.let { "unknown-c$it" } ?: "unknown-h${m.info.hashCode()}"
528535 } else {