diff --git a/app/radio.py b/app/radio.py index ac58834..a9b19e2 100644 --- a/app/radio.py +++ b/app/radio.py @@ -192,6 +192,9 @@ class RadioManager: if not blocking: if self._operation_lock.locked(): raise RadioOperationBusyError(f"Radio is busy (operation: {name})") + # In single-threaded asyncio the lock cannot be acquired between the + # check above and the await below (no other coroutine runs until we + # yield). The await returns immediately for an uncontested lock. await self._operation_lock.acquire() else: await self._operation_lock.acquire() diff --git a/app/repository/messages.py b/app/repository/messages.py index d83e947..65c4799 100644 --- a/app/repository/messages.py +++ b/app/repository/messages.py @@ -557,10 +557,11 @@ class MessageRepository: @staticmethod async def increment_ack_count(message_id: int) -> int: """Increment ack count and return the new value.""" - await db.conn.execute("UPDATE messages SET acked = acked + 1 WHERE id = ?", (message_id,)) - await db.conn.commit() - cursor = await db.conn.execute("SELECT acked FROM messages WHERE id = ?", (message_id,)) + cursor = await db.conn.execute( + "UPDATE messages SET acked = acked + 1 WHERE id = ? RETURNING acked", (message_id,) + ) row = await cursor.fetchone() + await db.conn.commit() return row["acked"] if row else 1 @staticmethod diff --git a/frontend/src/hooks/useConversationMessages.ts b/frontend/src/hooks/useConversationMessages.ts index 5c11b80..660c5da 100644 --- a/frontend/src/hooks/useConversationMessages.ts +++ b/frontend/src/hooks/useConversationMessages.ts @@ -372,6 +372,8 @@ export function useConversationMessages( const olderAbortControllerRef = useRef(null); const newerAbortControllerRef = useRef(null); const fetchingConversationIdRef = useRef(null); + const activeConversationRef = useRef(activeConversation); + activeConversationRef.current = activeConversation; const latestReconcileRequestIdRef = useRef(0); const pendingReconnectReconcileRef = useRef(false); const messagesRef = useRef([]); @@ -664,9 +666,11 @@ export function useConversationMessages( }, [activeConversation]); const reconcileOnReconnect = useCallback(() => { - if (!isMessageConversation(activeConversation)) { - return; - } + // Read the current conversation from the ref rather than closing over + // activeConversation, so that a conversation switch during WS reconnect + // targets the right conversation instead of a stale capture. + const current = activeConversationRef.current; + if (!isMessageConversation(current)) return; if (hasNewerMessagesRef.current) { pendingReconnectReconcileRef.current = true; @@ -677,8 +681,8 @@ export function useConversationMessages( const controller = new AbortController(); const requestId = latestReconcileRequestIdRef.current + 1; latestReconcileRequestIdRef.current = requestId; - reconcileFromBackend(activeConversation, controller.signal, requestId); - }, [activeConversation, reconcileFromBackend]); + reconcileFromBackend(current, controller.signal, requestId); + }, [reconcileFromBackend]); useEffect(() => { if (abortControllerRef.current) {