fix(chat): Such-Scroll + Doppel-Send-Hang + Delivery-Handshake
Drei Etappen Chat-Fixes: Etappe 1 — Such-Scroll permanent springen weg: - invertedMessages raus aus dem useEffect-Deps; neue ARIA-Nachrichten triggern den Scroll-Effect nicht mehr. Aktueller Snapshot via Ref. - onScrollToIndexFailed: statt 3 cascading Retries (120/320/600ms) nur noch EINE Retry nach 300ms. Cascading-Retries waren der Endlos-Cascade-Bug (jeder Failed-Retry triggerte 3 weitere). Etappe 2 — AsyncStorage-Race + Stuck-Thinking: - Init-Load merged statt overwrite — Nachrichten die zwischen Mount und Load-Done reinkommen werden nicht mehr verschluckt. - Stuck-Thinking-Watchdog: 180s ohne agent_activity-Update → Auto-Reset auf idle + Timeout-Bubble. Gegen "App haengt auf 'ARIA denkt'". Etappe 3 — Delivery-Handshake (WhatsApp-Style): - Pro User-Bubble: clientMsgId + deliveryStatus (queued/sending/sent/delivered/failed). - Offline-Queue: Send waehrend disconnected → 'queued' → flush bei Reconnect. - Bridge sendet chat_ack zurueck → Bubble auf 'sent' (✓). - ARIA-Reply → alle vorigen User-Bubbles 'delivered' (✓✓). - ACK-Timeout 30s, bis zu 3 Retries, danach 'failed' (rotes Tap-fuer-Retry). - Bridge: LRU-Idempotenz (200 cmids) verhindert Doppelte beim Retry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import time
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -475,6 +476,13 @@ class ARIABridge:
|
||||
self.current_mode = self._load_persisted_mode()
|
||||
self.running = False
|
||||
|
||||
# Idempotenz: zuletzt gesehene clientMsgIds (App-seitig generiert).
|
||||
# Beim Reconnect/Retry sendet die App dieselbe ID nochmal — wir
|
||||
# antworten erneut mit ACK aber leiten NICHT doppelt an Brain weiter.
|
||||
# OrderedDict als FIFO mit Capping (Insertion-Order).
|
||||
self._seen_client_msg_ids: "OrderedDict[str, float]" = OrderedDict()
|
||||
self._SEEN_CLIENT_MSG_LIMIT = 200
|
||||
|
||||
# Komponenten (TTS: F5-TTS remote auf der Gamebox, lokales TTS wurde entfernt)
|
||||
self.tts_enabled = True
|
||||
self.xtts_voice = ""
|
||||
@@ -1530,6 +1538,36 @@ class ARIABridge:
|
||||
except Exception:
|
||||
break
|
||||
|
||||
async def _send_chat_ack(self, client_msg_id: Optional[str]) -> None:
|
||||
"""Bestaetigt der App den Empfang einer chat/audio-Nachricht.
|
||||
App nutzt das fuer Delivery-Status (✓ = sent). Ohne ACK wuerde die
|
||||
App nach Timeout retryen — gegen Verlust bei Netz-Hicksern.
|
||||
"""
|
||||
if not client_msg_id:
|
||||
return
|
||||
await self._send_to_rvs({
|
||||
"type": "chat_ack",
|
||||
"payload": {"clientMsgId": client_msg_id},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
|
||||
def _is_duplicate_client_msg(self, client_msg_id: Optional[str]) -> bool:
|
||||
"""Prueft ob wir diese clientMsgId schon verarbeitet haben.
|
||||
Wenn ja → True (Caller soll ACK senden aber NICHT an Brain forwarden).
|
||||
Wenn nein → in den Seen-Cache aufnehmen + False zurueck.
|
||||
"""
|
||||
if not client_msg_id:
|
||||
return False
|
||||
if client_msg_id in self._seen_client_msg_ids:
|
||||
logger.info("[rvs] Idempotenz: cmid=%s bereits verarbeitet, ignoriere",
|
||||
client_msg_id)
|
||||
return True
|
||||
self._seen_client_msg_ids[client_msg_id] = time.time()
|
||||
# Capping: aelteste Eintraege rauswerfen
|
||||
while len(self._seen_client_msg_ids) > self._SEEN_CLIENT_MSG_LIMIT:
|
||||
self._seen_client_msg_ids.popitem(last=False)
|
||||
return False
|
||||
|
||||
async def _handle_rvs_message(self, raw_message: str) -> None:
|
||||
"""Verarbeitet Nachrichten von der App (via RVS).
|
||||
|
||||
@@ -1554,6 +1592,13 @@ class ARIABridge:
|
||||
sender = payload.get("sender", "")
|
||||
if sender in ("aria", "stt"):
|
||||
return
|
||||
# Delivery-ACK: immer zurueckschicken (auch bei Idempotenz-Hit),
|
||||
# damit die App den Status auf 'sent' setzen kann. Idempotenz-
|
||||
# Check VERHINDERT aber die Doppel-Weiterleitung an Brain.
|
||||
client_msg_id = payload.get("clientMsgId") or None
|
||||
await self._send_chat_ack(client_msg_id)
|
||||
if self._is_duplicate_client_msg(client_msg_id):
|
||||
return
|
||||
text = payload.get("text", "")
|
||||
# Voice-Override fuer Folgenachrichten setzen — gilt bis zum naechsten
|
||||
# chat-Event. Leerer String "" = explizit Default-Voice (override loeschen).
|
||||
@@ -2153,6 +2198,12 @@ class ARIABridge:
|
||||
|
||||
elif msg_type == "audio":
|
||||
# Audio von der App → decodieren → STT → an aria-core
|
||||
# Delivery-ACK + Idempotenz wie bei chat — App nutzt die ACKs
|
||||
# auch fuer Sprach-Bubbles (Status auf der Bubble: ✓ sent).
|
||||
client_msg_id = payload.get("clientMsgId") or None
|
||||
await self._send_chat_ack(client_msg_id)
|
||||
if self._is_duplicate_client_msg(client_msg_id):
|
||||
return
|
||||
audio_b64 = payload.get("base64", "")
|
||||
mime_type = payload.get("mimeType", "audio/mp4")
|
||||
duration_ms = payload.get("durationMs", 0)
|
||||
|
||||
Reference in New Issue
Block a user