From b70623bb7890090417c03cfb9c3bbc140435cb8c Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 2 Oct 2026 01:48:27 +0200 Subject: [PATCH] =?UTF-8?q?fix(app):=20Absturzschleife=20=E2=80=94=20Auto-?= =?UTF-8?q?Re-Download=20holte=20ungefragt=20ein=20150-MB-Video?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Absturzschleife (alle ~70s, stundenlang) war selbsttragend: App startet → Auto-Re-Download fordert 2s spaeter ALLE fehlenden Anhaenge an, ohne Groessen-Grenze → Bridge schickt das 150-MB-Video als ~200 MB base64 in EINER WebSocket-Nachricht → die App muss das puffern und durch JSON.parse schicken → nativer OOM (kein JS-Fehler, darum nie ein Stacktrace) → Android startet den Vordergrund-Dienst neu → App startet → von vorn. Dieselbe Nachricht hat parallel whisper/f5tts/voxtral mit Code 1009 umgebracht — darum lag der Absturz immer direkt hinter dem Audio-Abbau, und darum kam jede gerade laufende Sprachaufnahme nie an. Drei Bremsen: - Auto-Re-Download ueberspringt alles ueber 25 MB. Bleibt als "150 MB — tippen zum Laden" stehen; der User entscheidet. - Ausdrueckliches Antippen bremst bei 64 MB mit einer ehrlichen Meldung samt Server-Pfad, statt die App zu killen. - Der Brotkrumen wird von der Crash-Meldung nicht mehr ueberschrieben. Bisher merkte sich der zweite Absturz nur noch "letzte Aktivitaet: Crash-Meldung" — in einer Schleife war nach dem ersten Durchlauf jede Spur weg. Diagnose kam aus /shared/logs/app.log (app_log via RVS), kein ADB noetig. Co-Authored-By: Claude Opus 5 (1M context) --- android/src/screens/ChatScreen.tsx | 85 +++++++++++++++++++++++++----- android/src/services/logger.ts | 9 +++- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index e2e0257..108893d 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -200,6 +200,28 @@ function formatDur(sec: number): string { return `${m}:${r.toString().padStart(2, '0')}`; } +// ─── Grenzen fuer den Anhang-Re-Download ──────────────────────────── +// +// Ein Re-Download kommt als base64 in EINER WebSocket-Nachricht. Die muss die +// App komplett im Speicher puffern und dann durch JSON.parse schicken — bei +// einem 150-MB-Video sind das ~200 MB base64, und das Handy stirbt daran mit +// einem nativen OOM (kein JS-Fehler, darum tauchte nie ein Stacktrace auf). +// +// Genau das war die Absturzschleife: App startet → Auto-Re-Download fordert +// ungefragt das 150-MB-Video an → OOM → Android startet den Vordergrund- +// Dienst neu → App startet → von vorn. Alle ~70s, stundenlang. +// +// Darum zwei Grenzen: +// AUTO — was die App OHNE Nachfrage holt. Klein halten; Bilder und +// Dokumente passen rein, Videos nicht. +// TAP — was sie auf ausdruecklichen Tipp holt. Hoeher, aber immer noch +// weit unter der Schmerzgrenze. Drueber gibt's eine ehrliche +// Meldung statt eines Absturzes. +const AUTO_DL_MAX_BYTES = 25 * 1024 * 1024; +const TAP_DL_MAX_BYTES = 64 * 1024 * 1024; + +const fmtMB = (n: number) => `${(n / 1024 / 1024).toFixed(0)} MB`; + const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`; const STORAGE_PATH_KEY = 'aria_attachment_storage_path'; @@ -425,6 +447,33 @@ const ChatScreen: React.FC = () => { // Intent geoeffnet (PDF-Viewer, Galerie, etc.). const autoOpenPaths = useRef>(new Set()); + /** + * Re-Download anfordern — mit Groessen-Bremse. + * Gibt false zurueck (und sagt warum), wenn die Datei zu gross fuer einen + * Transfer durch die WebSocket ist. Ohne das holt sich die App ein 150-MB- + * Video als ~200 MB base64 und stirbt am nativen OOM (siehe TAP_DL_MAX_BYTES). + */ + const requestAttachment = useCallback((att: Attachment, reqId: string): boolean => { + if (!att.serverPath) { + if (Platform.OS === 'android') { + ToastAndroid.show('Datei kann nicht nachgeladen werden (kein serverPath)', ToastAndroid.LONG); + } + return false; + } + if (typeof att.size === 'number' && att.size > TAP_DL_MAX_BYTES) { + if (Platform.OS === 'android') { + ToastAndroid.show( + `${att.name}: ${fmtMB(att.size)} — zu gross fuers Handy. ` + + `Liegt auf dem Server unter ${att.serverPath}`, + ToastAndroid.LONG, + ); + } + return false; + } + rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: reqId }); + return true; + }, []); + // Eindeutige Message-ID generieren const nextId = (): string => { messageIdCounter.current += 1; @@ -860,13 +909,26 @@ const ChatScreen: React.FC = () => { setTimeout(() => { setMessages(prev => { const missing: {id: string, serverPath: string}[] = []; + const skipped: string[] = []; for (const msg of prev) { for (const att of msg.attachments || []) { - if (att.serverPath && !att.uri) { - missing.push({ id: msg.id, serverPath: att.serverPath }); + if (!att.serverPath || att.uri) continue; + // Grosse Dateien NICHT ungefragt holen (siehe AUTO_DL_MAX_BYTES). + // Sie bleiben als "tippen zum Laden" stehen — der User + // entscheidet, ob er sie wirklich aufs Handy zieht. + if (typeof att.size === 'number' && att.size > AUTO_DL_MAX_BYTES) { + skipped.push(`${att.name} (${fmtMB(att.size)})`); + continue; } + missing.push({ id: msg.id, serverPath: att.serverPath }); } } + if (skipped.length > 0) { + console.log('[Chat] Auto-Download uebersprungen (zu gross): %s', skipped.join(', ')); + import('../services/logger') + .then(m => m.reportAppDebug('chat.autodl', `uebersprungen: ${skipped.join(', ')}`)) + .catch(() => {}); + } if (missing.length > 0) { console.log(`[Chat] ${missing.length} fehlende Anhaenge — lade nach...`); for (const m of missing) { @@ -2869,16 +2931,15 @@ const ChatScreen: React.FC = () => { ) : att.type === 'image' && !att.uri ? ( { - if (att.serverPath) { - rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id }); - } - }} + onPress={() => { requestAttachment(att, item.id); }} > {'\uD83D\uDDBC\uFE0F'} {att.name} - {att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'} + {!att.serverPath ? '(nicht verfuegbar)' + : typeof att.size === 'number' && att.size > AUTO_DL_MAX_BYTES + ? `(${fmtMB(att.size)} — tippen zum Laden)` + : '(tippen zum Laden)'} ) : att.type === 'audio' ? ( @@ -2915,11 +2976,9 @@ const ChatScreen: React.FC = () => { } // Re-Download via file_request \u2192 bei file_response wird die // Datei gespeichert UND geoeffnet (autoOpenPaths-Tracking). - if (att.serverPath) { - autoOpenPaths.current.add(att.serverPath); - rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id }); - } else if (Platform.OS === 'android') { - ToastAndroid.show('Datei kann nicht nachgeladen werden (kein serverPath)', ToastAndroid.LONG); + if (att.serverPath) autoOpenPaths.current.add(att.serverPath); + if (!requestAttachment(att, item.id) && att.serverPath) { + autoOpenPaths.current.delete(att.serverPath); } }} > diff --git a/android/src/services/logger.ts b/android/src/services/logger.ts index 54ab10e..a1e77dc 100644 --- a/android/src/services/logger.ts +++ b/android/src/services/logger.ts @@ -49,8 +49,15 @@ const BREADCRUMB_KEY = 'aria_last_breadcrumb'; let _breadcrumb: { ts: number; scope: string; message: string } = { ts: 0, scope: '', message: '' }; let _breadcrumbDirty = false; -/** Letzte App-Aktivitaet merken — Crash-Kontext fuer den naechsten Boot. */ +/** Letzte App-Aktivitaet merken — Crash-Kontext fuer den naechsten Boot. + * + * Die Crash-Meldung selbst darf den Brotkrumen NICHT ueberschreiben: sonst + * merkt sich der zweite Absturz nur noch "letzte Aktivitaet: Crash-Meldung", + * der dritte "letzte Aktivitaet: Crash-Meldung ueber Crash-Meldung" — und bei + * einer Absturzschleife ist nach dem ersten Durchlauf jede Spur weg. Genau + * das ist passiert; die eigentliche Ursache stand nur in der ERSTEN Meldung. */ export function noteBreadcrumb(scope: string, message: string): void { + if (scope === 'app.crash-detected') return; _breadcrumb = { ts: Date.now(), scope: scope || '', message: String(message || '').slice(0, 120) }; _breadcrumbDirty = true; }