diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index e3135b3..62d2012 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -2371,6 +2371,10 @@ const ChatScreen: React.FC = () => { size: file.size, base64, projectId: activePid, + // Korrelation: dieselbe clientMsgId wie der Text, damit die Bridge + // die Datei genau DIESER Nachricht zuordnet — auch wenn Files (fire- + // and-forget) und Text (ACK-getrackt, bei Queue verzoegert) desyncen. + ...(cmid && { clientMsgId: cmid }), ...(isPhoto && file.width && { width: file.width, height: file.height }), ...(location && { location }), }); diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index c2bea39..4b38a0a 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -790,7 +790,8 @@ class ARIABridge: # Anfrage an aria-core. Sonst antwortet ARIA zweimal (einmal "warte auf # Anweisung" beim file, einmal auf den Chat-Text). # Liste von Tuples: (file_path, name, file_type, size_kb, width, height) - self._pending_files: list[tuple[str, str, str, int, int, int]] = [] + # (file_path, name, type, kb, width, height, clientMsgId) + self._pending_files: list[tuple[str, str, str, int, int, int, str]] = [] self._pending_files_flush_task: Optional[asyncio.Task] = None # Projekt-Kontext der gerade gepufferten Anhaenge (aus dem file-Upload). # Wird beim Flush an send_to_core gegeben, damit Anhaenge im richtigen @@ -1839,16 +1840,16 @@ class ARIABridge: return " ".join(parts) + " " + text return text - def _build_pending_files_message(self, user_text: str) -> str: - """Baut eine Anweisung an aria-core aus den gepufferten Files + optionalem + def _build_pending_files_message(self, user_text: str, files: list) -> str: + """Baut eine Anweisung an aria-core aus den uebergebenen Files + optionalem User-Text. user_text leer → 'warte auf Anweisung'-Variante.""" parts: list[str] = [] - for fp, name, ftype, kb, w, h in self._pending_files: + for fp, name, ftype, kb, w, h, _cmid in files: dim = f" {w}x{h}px" if (w and h) else "" kind = "Bild" if ftype.startswith("image/") else "Datei" parts.append(f"- {kind}: {name}{dim} ({ftype}, {kb}KB) liegt unter {fp}") files_summary = "\n".join(parts) - n = len(self._pending_files) + n = len(files) anhang = "Anhang" if n == 1 else "Anhaenge" if user_text: return (f"Stefan hat dir {n} {anhang} geschickt:\n{files_summary}\n\n" @@ -1857,15 +1858,16 @@ class ARIABridge: f"Warte auf seine Anweisung was du damit tun sollst.") async def _flush_pending_files_after(self, delay: float) -> None: - """Wenn nach `delay`s kein chat-Text gekommen ist: Files alleine an - aria-core senden ('warte auf Anweisung'-Variante).""" + """Wenn nach `delay`s kein chat-Text gekommen ist: alle noch gepufferten + Files alleine an aria-core senden ('warte auf Anweisung'-Variante).""" try: await asyncio.sleep(delay) except asyncio.CancelledError: return if not self._pending_files: return - text = self._build_pending_files_message("") + files = self._pending_files + text = self._build_pending_files_message("", files) self._pending_files = [] self._pending_files_flush_task = None pid = self._pending_files_project_id @@ -1873,23 +1875,48 @@ class ARIABridge: await self.send_to_core(text, source="app-file", project_id=pid) async def _flush_pending_files_with_text(self, user_text: str, - project_id: str = "") -> bool: + project_id: str = "", + client_msg_id: str = "") -> bool: """Wenn ein chat-Text reinkommt waehrend Files gepuffert sind: Files + Text zu einer einzigen aria-core-Nachricht mergen. Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden). + KORRELATION (Fix Queue-Bug): Files tragen dieselbe clientMsgId wie ihr + Text. Bei einer Queue gehen Files (fire-and-forget) und Text (ACK- + getrackt, ggf. verzoegert) auseinander — ohne Korrelation landeten die + Bilder beim falschen Text. Wir mergen darum NUR die Files mit passender + cmid; der Rest bleibt gepuffert fuer seine eigene Nachricht. Fallback + (Legacy-App ohne cmid an Files, oder cmid ohne Treffer): altes Verhalten + (alle Files mit diesem Text), damit nie ein Bild verloren geht. + project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus - beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext - zurueck, damit Anhaenge im richtigen Projekt landen statt im Hauptchat.""" + beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext zurueck. + """ if not self._pending_files: return False + cmid = (client_msg_id or "").strip() + matching = [f for f in self._pending_files if cmid and f[6] == cmid] + if not matching: + # Kein cmid-Treffer → altes Verhalten: alle gepufferten Files mergen. + matching = list(self._pending_files) + remaining: list = [] + else: + remaining = [f for f in self._pending_files if f not in matching] + + text = self._build_pending_files_message(user_text, matching) + self._pending_files = remaining + pid = (project_id or "").strip() or self._pending_files_project_id + # Flush-Timer neu setzen wenn noch Files anderer Nachrichten warten, + # sonst zuruecksetzen. if self._pending_files_flush_task and not self._pending_files_flush_task.done(): self._pending_files_flush_task.cancel() self._pending_files_flush_task = None - text = self._build_pending_files_message(user_text) - self._pending_files = [] - pid = (project_id or "").strip() or self._pending_files_project_id - self._pending_files_project_id = "" + if remaining: + self._pending_files_flush_task = asyncio.create_task( + self._flush_pending_files_after(self._PENDING_FILES_WINDOW_SEC) + ) + else: + self._pending_files_project_id = "" # create_task statt await — sonst blockt der RVS-recv-Loop bis Brain # fertig ist (siehe chat-handler oben). asyncio.create_task(self.send_to_core(text, source="app-file+chat", project_id=pid)) @@ -2364,7 +2391,8 @@ class ARIABridge: # gesendet), mergen wir sie zu einer einzigen Anfrage statt # zwei separater send_to_core-Calls. merged = await self._flush_pending_files_with_text( - text, project_id=str(payload.get("projectId") or "")) + text, project_id=str(payload.get("projectId") or ""), + client_msg_id=client_msg_id or "") if merged: logger.info("[rvs] App-Chat (mit Anhaengen) project=%s: '%s'", str(payload.get("projectId") or "") or "(main)", text[:80]) @@ -2658,8 +2686,11 @@ class ARIABridge: logger.warning("[rvs] Bild-Resize fehlgeschlagen (%s) — Original wird genutzt: %s", file_name, e) - # In Pending-Queue + Flush-Timer (anti-spam Buffering) - self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0))) + # In Pending-Queue + Flush-Timer (anti-spam Buffering). + # clientMsgId mitpuffern → spaeterer Text-Flush ordnet die Datei + # genau SEINER Nachricht zu (Queue-Korrelation, s. _flush_*). + file_cmid = str(payload.get("clientMsgId") or "") + self._pending_files.append((file_path, file_name, file_type, size_kb, int(width or 0), int(height or 0), file_cmid)) if self._pending_files_flush_task and not self._pending_files_flush_task.done(): self._pending_files_flush_task.cancel() self._pending_files_flush_task = asyncio.create_task(