From 6cbbea9b96db551e5c0ac26f0ace3d4c3d82a276 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Mon, 06 Apr 2026 15:02:33 +0200
Subject: [PATCH] fix: single pailot/out topic, per-session file locks, merge protection, resume reconnect

---
 lib/services/message_store.dart |   27 ++++++++++++++++++++++++---
 1 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/lib/services/message_store.dart b/lib/services/message_store.dart
index f45e0d0..1723b70 100644
--- a/lib/services/message_store.dart
+++ b/lib/services/message_store.dart
@@ -15,6 +15,8 @@
   static Directory? _baseDir;
   static Timer? _debounceTimer;
   static final Map<String, List<Message>> _pendingSaves = {};
+  // Per-session lock to prevent concurrent read/write on the same file
+  static final Map<String, Completer<void>> _locks = {};
 
   static const _backupChannel =
       MethodChannel('com.mnsoft.pailot/backup');
@@ -60,10 +62,25 @@
 
   /// Write directly to disk, bypassing debounce. For critical saves.
   static Future<void> writeDirect(String sessionId, List<Message> messages) async {
-    // Cancel ALL pending debounce to prevent race with frozen iOS timers
     _debounceTimer?.cancel();
     _pendingSaves.remove(sessionId);
-    await _writeSession(sessionId, messages);
+    await _withLock(sessionId, () => _writeSession(sessionId, messages));
+  }
+
+  /// Acquire a per-session lock, run the operation, release.
+  static Future<T> _withLock<T>(String sessionId, Future<T> Function() fn) async {
+    // Wait for any existing operation on this session to finish
+    while (_locks.containsKey(sessionId)) {
+      await _locks[sessionId]!.future;
+    }
+    final completer = Completer<void>();
+    _locks[sessionId] = completer;
+    try {
+      return await fn();
+    } finally {
+      _locks.remove(sessionId);
+      completer.complete();
+    }
   }
 
   /// Immediately flush all pending saves.
@@ -77,7 +94,7 @@
     _pendingSaves.clear();
 
     for (final entry in entries.entries) {
-      await _writeSession(entry.key, entry.value);
+      await _withLock(entry.key, () => _writeSession(entry.key, entry.value));
     }
   }
 
@@ -128,6 +145,10 @@
 
   /// Load all messages for a session (no pagination).
   static Future<List<Message>> loadAll(String sessionId) async {
+    return _withLock(sessionId, () => _loadAllImpl(sessionId));
+  }
+
+  static Future<List<Message>> _loadAllImpl(String sessionId) async {
     try {
       final dir = await _getBaseDir();
       final file = File('${dir.path}/${_fileForSession(sessionId)}');

--
Gitblit v1.3.1