Compare commits

...

20 Commits

Author SHA1 Message Date
duffyduck 853f2737f1 release: bump version to 0.1.4.2 2026-05-14 22:29:31 +02:00
duffyduck 7c61107f87 release: bump version to 0.1.4.1 2026-05-14 22:16:17 +02:00
duffyduck 7a22474efd feat(chat): Jump-down-Button + Sprung-an-Text-Anfang + Vision-Issue raus
Drei kleine UX-Fixes im Chat:

1. Jump-Down-Button (↓): Bei inverted FlatList erscheint rechts ueber
   der Eingabe ein blauer FAB, sobald man mehr als 250px von der
   neuesten Nachricht weg gescrollt ist. Tap → scrollToOffset(0)
   animated → wieder unten. Auto-hide wenn man unten ist.

2. Such-Sprung landet jetzt am TEXT-ANFANG der Treffer-Bubble:
   viewPosition 0.5 (Mitte) → 0 (Item-Top am Viewport-Top). Plus
   Retry-Folge (180/420/800ms) gegen Layout-Race bei langen Listen.
   Vorher musste man oft nochmal hoch scrollen um den Anfang zu sehen.
   onScrollToIndexFailed-Fallback genauso mit viewPosition 0.

3. issue.md: "Bilder: Claude Vision direkt nutzen" raus aus den
   offenen Punkten — ist durch Stufe E (Memory-Anhaenge, Read-Tool
   multi-modal) längst geloest. ARIA sieht Bilder echt.

Folge-Etappen: Such-Sprung-Resilienz war Teil davon (mehrere Retries
abgedeckt). Naechste Brocken: Doppel-Send-Haenger, AsyncStorage-Race,
Offline-Queue mit Idempotenz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:10:26 +02:00
duffyduck f2cf4e0d58 release: bump version to 0.1.4.0 2026-05-14 18:39:58 +02:00
duffyduck db4bebfa57 docs: README + issue — drei GPS-Trigger-Modi + Tick-Frequenz-Fix
README.md:
- Diagnostic-Trigger-Tab-Beschreibung erweitert um die drei GPS-Funktionen
  (near / entered_near / left_near) mit Use-Cases pro Modus
- Plus Auflösung erklaert: 8s-Tick + event-getrieben bei location_update
  fuer Auto-Vorbeifahrten. 5-min-Age-Schutz gegen Phantom-Fires
- Phase B Punkt 5 in der Roadmap entsprechend nachgezogen

issue.md: neuer Block "GPS-Trigger-Verbesserungen" mit drei Punkten —
Timing-Fix, Age-Schutz, drei Modi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:33:04 +02:00
duffyduck 435b77e1df feat(trigger): entered_near + left_near — drei Modi fuer near()-Watcher
Stefan: bei aktuellen near()-Watcher gibt's nur "solange drin". Reale
Szenarien wollen aber differenzieren:
- VORWARNUNG vor Ziel (Blitzer-Warner 2 km vorher) → entered_near mit grossem r
- ANKUNFT exakt am Ziel → entered_near mit kleinem r
- VERLASSEN (Parkplatz, hast du was vergessen) → left_near
- KONTINUIERLICH-DRIN (bin noch in der Naehe?) → near (Default, throttled)

Zwei neue Funktionen in der Condition-Whitelist:

- entered_near(lat, lon, r): True NUR im Moment des Uebergangs
  draussen → innen. Fires einmal pro Eintritt.
- left_near(lat, lon, r): True NUR im Moment des Uebergangs innen →
  draussen. Fires einmal pro Austritt.

State-Tracking:
- pro Trigger pro near-Aufruf wird der letzte Auswertungs-Wert (true/
  false) im Watcher-Manifest gespeichert (Field "near_states", Key
  "lat.6,lon.6,radius"). Background-Loop liest's vor dem Eval, gibt's
  per collect_variables(prev_near_states=...) in die Closure, schreibt
  nach dem Eval die neuen Werte zurueck — UNABHAENGIG ob gefeuert
  wurde, sonst greift die Uebergangs-Erkennung nicht.

Background _tick:
- Aufteilung in Watcher-Pass (mit prev_near_states pro Trigger) und
  Timer-Pass (ohne State, gemeinsame vars). Bisher war collect_variables
  einmal pro Tick — jetzt einmal pro Watcher. Disk-Stats sind teuer
  aber unter 30 Watchern unkritisch; bei mehr koennen wir cachen.

ARIA-Tool-Description erweitert (trigger_watcher): erklaert die drei
Modi mit Use-Cases und empfohlenen Throttle-Werten (kurz fuer entered/
left, lang fuer near).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:29:39 +02:00
duffyduck 6f80e442cf fix(trigger): near() fired bei Auto-Vorbeifahrten verpasst — Loop schneller + event-getrieben
Stefan ist mehrmals an einem 300m-near()-Watcher (DRK Kreyenbrueck)
vorbeigefahren, kein Fire. Ursache: Background-Loop tickte alle 30s,
Auto-Durchfahrt durch 600m-Durchmesser-Radius dauert bei 50-120 km/h
nur 18-43 Sekunden — der Tick konnte komplett dazwischen liegen.

Drei Fixes (A + B aus Stefans Vorschlag):

A1. Background-Loop-Frequenz: TICK_SEC 30 → 8.
    Garantiert mind. 2 Checks auch bei 120 km/h durch 300m. Loop ist
    billig (paar Dateilesungen + AST-Eval), Brain merkt das nicht.

A2. near() bekommt Age-Schutz (watcher.py NEAR_MAX_AGE_SEC=300):
    Wenn location_age_sec > 5 min, gilt die Position als unbekannt
    und near() liefert False. Verhindert Phantom-Fires wenn Tracking
    aus ist oder Mobilfunk weg war — vorher haette der letzte
    bekannte Wert weiter ausgewertet werden koennen.

B. Event-getriebener Tick:
    - background.py: tick_now()-Funktion + Module-Slot fuer
      agent_factory damit man von ausserhalb des Lifespan-Pfads
      einen Tick triggern kann
    - main.py: POST /triggers/check-now Endpoint ruft tick_now()
    - bridge: _persist_location feuert nach jedem Save ein fire-and-
      forget POST /triggers/check-now (run_in_executor, timeout 8s,
      blockt nichts wenn Brain stockt)

Damit fires near() sofort wenn die App ein location_update schickt —
Polling ist nur noch der Fallback fuer Watcher OHNE GPS-Bezug
(disk_free, hour_of_day etc.) und als Sicherheits-Tick falls
location_update mal ausfaellt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:16:53 +02:00
duffyduck 0fcbf5e3ed docs: README + issue — Memory-Editor-App + Crash-Reporting + Bugfixes
Heute Tag-2 nach dem Memory-Editor-Hauptbau:

issue.md: neuer Block "App-Memory-Editor + Crash-Reporting" mit 8
Punkten (Bubble-Header dynamic, Tap-Modal, Inbox, Settings-Editor,
RVS-Brain-Proxy, App-Crash-Reporting, memory_search+update Tools,
Bugfixes-Cluster).

README.md:
- App-Features um Notizen-Inbox + Memory-Editor + Bubble-Header
  dynamic + App-Crash-Reporting ergaenzt
- Roadmap um "Memory-Editor in der App" und "App-Crash-Reporting via
  RVS" als eigene Bullets — beide sitzen unter dem letzten
  Memory-Anhaenge-Eintrag und schliessen damit den App-UX-Loop:
  ARIA hat jetzt im Diagnostic UND in der App vollwertiges Memory-
  CRUD inkl. Anhaenge, plus Crashes sind ohne ADB diagnostizierbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:30:15 +02:00
duffyduck 3cf6308b79 release: bump version to 0.1.3.9 2026-05-14 17:17:44 +02:00
duffyduck 7e5a4da659 fix(app): Memory-Liste in Settings scrollt jetzt (nestedScrollEnabled)
Stefan: Memory-Liste in Settings → Gedaechtnis-Sektion laesst sich
nicht scrollen. Klassisches FlatList-in-ScrollView-Problem auf
Android: die aeussere ScrollView (Settings-Screen-Container) faengt
alle Gesten ab, die innere FlatList (MemoryBrowser) bleibt regungslos.

Fix:
- MemoryBrowser FlatList bekommt nestedScrollEnabled={true}
- SettingsScreen-aeussere-ScrollView ebenfalls nestedScrollEnabled
- Plus keyboardShouldPersistTaps="handled" damit Taps auf Filter-
  Buttons nicht von der Tastatur weggefangen werden

In der Inbox-Modal-Nutzung ist's egal — dort hat MemoryBrowser
flex:1 und der Container ist kein ScrollView.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:14:21 +02:00
duffyduck d27fcaf342 fix(chat): Cache leer + Datei-Tap → Auto-Re-Download statt Fehlertoast
Stefan: nach App-Cache-Leeren in Settings tippt er auf eine Datei im
Chat oder im memorySaved-Anhang → "Oeffnen fehlgeschlagen" Toast,
aber kein Re-Download. Datei hing als Geister-uri im State (RNFS-Cache
weg, State-attachment.uri zeigt noch auf den entfernten Pfad).

Fix in beiden Tap-Handlern (item.attachments im normalen Chat +
memorySaved.attachments via localUri):

1. RNFS.exists(localPath) pruefen
2. Wenn ja → openFileWithIntent
3. Wenn nein:
   - State bereinigen (uri/localUri auf undefined setzen, UI zeigt
     dann "tippen zum Laden")
   - Toast "Cache leer — lade nach..."
   - file_request via RVS triggern
   - autoOpenPaths-Marker setzen, sodass file_response → Datei
     speichern + automatisch oeffnen

Bilder-Branch hatte schon onError-Handler (Image-Komponente meldet
self) — der ist unveraendert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:14:56 +02:00
duffyduck 5b28a065c0 release: bump version to 0.1.3.7 2026-05-14 16:08:30 +02:00
duffyduck e74e1eaf70 fix(app): URLSearchParams crasht in Hermes — durch Mini-Query-Builder ersetzt
Inbox-Crash gefunden via App-Crash-Reporter (commit 21a315c):

  "URLSearchParams.set is not implemented"
  at MemoryBrowser → brainApi.listMemories

React Native's Hermes-Polyfill kennt zwar new URLSearchParams() aber
nicht die .set()-Methode darauf. Pickup-Bug — auf iOS / aelteren
Versionen geht's, Stefan's Android-Build crasht.

Fix: kleine _qs()-Helper im brainApi.ts der einen Query-String aus
einem flachen Object baut, ohne URLSearchParams:
  _qs({q:'cessna', k:5, type:'fact'}) → "?q=cessna&k=5&type=fact"

Plus: undefined/null/empty Werte werden ausgelassen — saubererer als
URLSearchParams.set wo man manuell prefilten muss.

ErrorBoundary aus 21a315c hat den Crash sauber abgefangen, statt der
App-Tot war ne Error-Box im Inbox-Modal mit der vollen Stack-Trace.
Stefan konnte den Log via tools/fetch-app-logs.sh holen ohne ADB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:06:21 +02:00
duffyduck ff7c6333bb release: bump version to 0.1.3.6 2026-05-14 16:00:42 +02:00
duffyduck 2c85df3499 chore: tools/fetch-app-logs.sh — App-Crash-Logs von der VM holen
Stefan ist unterwegs, ADB nicht moeglich. Dieses Script ist die andere
Haelfte des Crash-Reportings (commit 21a315c hat die App-Seite + Bridge-
Endpoint gebaut):

Nutzung:
  tools/fetch-app-logs.sh                # 200 letzte Eintraege
  tools/fetch-app-logs.sh --limit 50
  tools/fetch-app-logs.sh --watch        # alle 5s pollen + Diff ausgeben
  tools/fetch-app-logs.sh --clear        # Log auf VM nach Abholen leeren

Liest $ARIA_DIAG_URL aus .claude/aria-vm.env, ruft GET /api/app-log.
Speichert komplette JSON-Response in .aria-debug/app-log-<ts>.json
(gitignored). Stdout zeigt kompakt: Uhrzeit, Level, Scope, Message,
erste 8 Stack-Frames pro Eintrag.

.gitignore: .aria-debug/ ist komplett ausgeschlossen (Crashes
koennen private Daten enthalten).

tools/README.md: kurze Doku des Workflows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:59:06 +02:00
duffyduck 6f11f28448 release: bump version to 0.1.3.5 2026-05-14 15:45:36 +02:00
duffyduck 21a315ca71 feat(debug): App-Crash-Reporting via RVS — Logs in der Diagnostic-UI
Stefan ist unterwegs, ADB-Zugriff nicht moeglich. Loesung: die App
loggt ihre eigenen Crashes via RVS, Bridge sammelt sie in
/shared/logs/app.log, Diagnostic-Server liefert sie als JSON.
Damit braucht's keinen ADB mehr — Crashes sind sofort vom Browser
(oder Claude per curl) lesbar.

Komponenten:

1. App components/ErrorBoundary.tsx
   - React-ErrorBoundary fuer kritische Sections
   - componentDidCatch → reportAppError (RVS-Send)
   - UI zeigt Error-Box statt White-Screen + Reset-Button

2. App services/logger.ts
   - reportAppError(scope, message, stack) → rvs.send('app_log', ...)
   - installGlobalCrashReporter() haengt sich an ErrorUtils.setGlobalHandler
     UND HermesInternal.enablePromiseRejectionTracker — fangt sowohl
     ungefangene Errors als auch unhandled Promise-Rejections
   - Konsole bleibt parallel aktiv (damit ADB im Dev-Build weiter
     was sieht)

3. App App.tsx: installGlobalCrashReporter() im useEffect zusammen
   mit initLogger.

4. App ChatScreen.tsx:
   - Inbox-Modal mit ErrorBoundary umschlossen (scope: InboxModal,
     onReset schliesst Modal)
   - MemoryDetailModal mit ErrorBoundary umschlossen
   - DetailModal wird nur noch konditional gerendert (memoryDetailId
     != null) statt immer visible-toggle — vermeidet potentielles
     Modal-Stacking-Problem

5. RVS server.js: ALLOWED_TYPES += "app_log"

6. Bridge aria_bridge.py:
   - elif msg_type == "app_log": haengt eine Zeile an
     /shared/logs/app.log (JSONL, jedes Item {ts, platform, level,
     scope, message, stack})
   - Plus log.info Hinweis fuer das normale Bridge-Log

7. Diagnostic server.js:
   - GET /api/app-log[?limit=N] → letzte N Eintraege als JSON
   - POST /api/app-log/clear → log-Datei loeschen

Workflow zum Debuggen des Inbox-Crashes:
  Stefan rebuilded App → drueckt Inbox → ErrorBoundary fangt den
  Crash (oder Global-Handler bei ungefangenem Error) → reportAppError
  → RVS → Bridge schreibt nach /shared/logs/app.log → Stefan
  oder Claude rufen GET /api/app-log auf → sehen Stacktrace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:42:55 +02:00
duffyduck d8b05082d6 release: bump version to 0.1.3.4 2026-05-14 14:58:47 +02:00
duffyduck de91073b2e release: bump version to 0.1.3.3 2026-05-14 14:08:03 +02:00
duffyduck e88b5f57bf fix(memory): Inbox-Crash auf Android — Modal-Stacking + Alert.prompt
Stefan: App crasht beim Tap auf Inbox-Button. Zwei Ursachen:

1. Modal-in-Modal-Stacking (Inbox-Modal enthielt MemoryBrowser, der
   wiederum ein MemoryDetailModal gerendered hat). Android Modal hat
   damit Probleme — der Native-Layer mag nur eine Modal-Instance
   gleichzeitig zuverlaessig.

2. MemoryBrowser nutzte Alert.prompt fuer "Neue Memory anlegen" —
   das ist iOS-only, Android wirft eine Warnung oder crasht.

Fix:
- MemoryBrowser bekommt optionalen onOpenMemory-Callback. Wenn der
  Parent diesen liefert, mounted MemoryBrowser KEIN eigenes
  DetailModal mehr. ChatScreen mountet das DetailModal nur einmal
  auf seiner Ebene; Inbox-Modal schliesst sich beim Tap und delegiert
  die ID an memoryDetailId-State. Damit ist immer maximal ein Modal
  aktiv.
- Alert.prompt durch eigenes kleines Dialog-Modal ersetzt: TextInput
  fuer Titel, Anlegen/Abbrechen-Buttons. Cross-platform stabil.

SettingsScreen-Nutzung von MemoryBrowser bleibt unveraendert (kein
Callback → eingebautes DetailModal, aber dort kein Modal-Stacking
weil Settings kein Modal ist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:05:42 +02:00
21 changed files with 821 additions and 117 deletions
+4
View File
@@ -25,6 +25,10 @@ aria-data/brain-import/*
!aria-data/brain-import/.gitkeep !aria-data/brain-import/.gitkeep
!aria-data/brain-import/README.md !aria-data/brain-import/README.md
# .aria-debug/ — App-Crash-Logs die tools/fetch-app-logs.sh hier ablegt.
# Komplett lokal, enthaelt potentiell private Stacktraces / Daten.
.aria-debug/
# ── ARIAs Gedächtnis (Vector-DB, Skills, Models) ── # ── ARIAs Gedächtnis (Vector-DB, Skills, Models) ──
# Backup via Diagnostic → Gehirn-Export (tar.gz), nicht via Git. # Backup via Diagnostic → Gehirn-Export (tar.gz), nicht via Git.
aria-data/brain/data/ aria-data/brain/data/
+15 -3
View File
@@ -200,7 +200,7 @@ Die Diagnostic-UI hat sechs Top-Tabs:
- **Main** — Live-Chat-Test, Status (Brain / RVS / Proxy), End-to-End-Trace - **Main** — Live-Chat-Test, Status (Brain / RVS / Proxy), End-to-End-Trace
- **Gehirn** — Memory-Verwaltung (Vector-DB), Token/Call-Metrics (Subscription-Quota), Bootstrap & Migration, Komplett-Gehirn Export/Import - **Gehirn** — Memory-Verwaltung (Vector-DB), Token/Call-Metrics (Subscription-Quota), Bootstrap & Migration, Komplett-Gehirn Export/Import
- **Skills** — Liste mit Logs, Run, Activate/Deactivate, Export/Import als tar.gz - **Skills** — Liste mit Logs, Run, Activate/Deactivate, Export/Import als tar.gz
- **Trigger** — Timer + Watcher anlegen/anzeigen/loeschen, Live-Variablen-Anzeige (disk_free, current_lat, hour_of_day, …), near(lat, lon, m) als Condition-Funktion - **Trigger** — Timer + Watcher anlegen/anzeigen/loeschen, Live-Variablen-Anzeige (disk_free, current_lat, hour_of_day, …), GPS-Funktionen `near() / entered_near() / left_near()` für unterschiedliche Geofencing-Modi
- **Dateien** — alle Dateien aus `/shared/uploads/` mit Multi-Select, Bulk-Download (ZIP) + Bulk-Delete - **Dateien** — alle Dateien aus `/shared/uploads/` mit Multi-Select, Bulk-Download (ZIP) + Bulk-Delete
- **Einstellungen** — Reparatur (Container-Restart), Wipe, Sprachausgabe, Whisper, Sprachmodell, Runtime-Config, App-Onboarding (QR), Komplett-Reset - **Einstellungen** — Reparatur (Container-Restart), Wipe, Sprachausgabe, Whisper, Sprachmodell, Runtime-Config, App-Onboarding (QR), Komplett-Reset
@@ -319,7 +319,14 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
- **Main**: Brain/RVS/Proxy-Status, Chat-Test, "ARIA denkt..."-Indikator, End-to-End-Trace, Container-Logs - **Main**: Brain/RVS/Proxy-Status, Chat-Test, "ARIA denkt..."-Indikator, End-to-End-Trace, Container-Logs
- **Gehirn**: Memory-Browser (Vector-DB), Suche mit zwei Modi (**📝 Wortlich** = Substring-Match Default + **🧠 Semantisch** mit Score-Threshold), **Advanced Search** (aufklappbares Panel, beliebig viele AND/OR-verknuepfte Felder, + Button fuer mehr Zeilen), Type+Pinned-Filter (greifen auch in der Suche), klappbare Type-Kategorien (Default eingeklappt), Add/Edit/Delete mit Category-Autosuggest, **📎 Anhaenge** pro Memory (Bilder/PDFs/...): Upload + Thumbnail-Vorschau + Lightbox + Lösch-Button, 📎N-Badge in der Liste, automatischer Cleanup beim Memory-Delete. -Info-Modal das erklaert welche Types FEST in den Prompt vs. Cold Memory wandern. **📄 Druckansicht** (Strg+P → PDF). Konversation-Status mit Destillat-Trigger, **Token/Call-Metrics mit Subscription-Quota-Tracking**, Bootstrap & Migration (3 Wiederherstellungs-Wege), Gehirn-Export/Import (tar.gz) - **Gehirn**: Memory-Browser (Vector-DB), Suche mit zwei Modi (**📝 Wortlich** = Substring-Match Default + **🧠 Semantisch** mit Score-Threshold), **Advanced Search** (aufklappbares Panel, beliebig viele AND/OR-verknuepfte Felder, + Button fuer mehr Zeilen), Type+Pinned-Filter (greifen auch in der Suche), klappbare Type-Kategorien (Default eingeklappt), Add/Edit/Delete mit Category-Autosuggest, **📎 Anhaenge** pro Memory (Bilder/PDFs/...): Upload + Thumbnail-Vorschau + Lightbox + Lösch-Button, 📎N-Badge in der Liste, automatischer Cleanup beim Memory-Delete. -Info-Modal das erklaert welche Types FEST in den Prompt vs. Cold Memory wandern. **📄 Druckansicht** (Strg+P → PDF). Konversation-Status mit Destillat-Trigger, **Token/Call-Metrics mit Subscription-Quota-Tracking**, Bootstrap & Migration (3 Wiederherstellungs-Wege), Gehirn-Export/Import (tar.gz)
- **Skills**: Liste aller Skills mit Logs pro Run, Activate/Deactivate, Export/Import als tar.gz, "von ARIA"-Badge fuer selbst gebaute - **Skills**: Liste aller Skills mit Logs pro Run, Activate/Deactivate, Export/Import als tar.gz, "von ARIA"-Badge fuer selbst gebaute
- **Trigger**: passive Aufweck-Quellen. **Timer** (einmalig, ISO-Timestamp oder via `in_seconds` als Server-Berechnung) + **Watcher** (recurring, mit Condition + Throttle). Liste aktiver Trigger + Logs pro Feuer-Event. Modal mit Type-Dropdown, Live-Anzeige aller verfuegbaren Condition-Variablen (`disk_free_gb`, `hour_of_day`, `current_lat/lon`, `last_user_message_ago_sec`, …) und Condition-Funktionen (`near(lat, lon, m)` fuer GPS-Geofencing). Sicherer Condition-Parser via Python `ast` (Whitelist, kein `eval`). Der System-Prompt enthaelt zusaetzlich einen `## Aktuelle Zeit`-Block (UTC + Europa/Berlin) damit ARIA Timer-Zeitpunkte korrekt setzen kann. - **Trigger**: passive Aufweck-Quellen. **Timer** (einmalig, ISO-Timestamp oder via `in_seconds` als Server-Berechnung) + **Watcher** (recurring, mit Condition + Throttle). Liste aktiver Trigger + Logs pro Feuer-Event. Modal mit Type-Dropdown, Live-Anzeige aller verfuegbaren Condition-Variablen (`disk_free_gb`, `hour_of_day`, `current_lat/lon`, `last_user_message_ago_sec`, …). **Drei GPS-Funktionen** mit unterschiedlicher Semantik:
- `near(lat, lon, r)` — SOLANGE im Radius (mit Throttle gegen Spam). Use-Case: „bin ich noch in der Nähe von X?"
- `entered_near(lat, lon, r)` — EINMAL beim Eintritt (Übergang außen→innen). Use-Case: Blitzer-Warner mit r=2000 → 2 km Vorwarnung, oder Ankunfts-Erinnerung mit r=100
- `left_near(lat, lon, r)` — EINMAL beim Verlassen (Übergang innen→außen). Use-Case: „Hast du am Parkplatz X was vergessen?"
Sicherer Condition-Parser via Python `ast` (Whitelist, kein `eval`). Der System-Prompt enthaelt zusaetzlich einen `## Aktuelle Zeit`-Block (UTC + Europa/Berlin) damit ARIA Timer-Zeitpunkte korrekt setzen kann.
**Auflösung**: Background-Loop tickt alle 8s (vorher 30s — bei 100 km/h durch einen 300m-Radius war eine Vorbeifahrt nur ~22s drin und konnte verpasst werden). Plus event-getrieben: Bridge ruft nach jedem `location_update` von der App sofort einen `/triggers/check-now` im Brain — Watcher sehen die frische Position in Millisekunden statt im Polling-Takt. `near()`-Funktionen ignorieren GPS-Daten älter als 5 Minuten (verhindert Phantom-Fires bei abgeschaltetem Tracking).
- **Dateien**: Browser fuer `/shared/uploads/` mit Multi-Select + "Alle markieren" + Bulk-Download (ZIP bei 2+) + Bulk-Delete. Live-Update der Chat-Bubbles beim Delete. - **Dateien**: Browser fuer `/shared/uploads/` mit Multi-Select + "Alle markieren" + Bulk-Download (ZIP bei 2+) + Bulk-Delete. Live-Update der Chat-Bubbles beim Delete.
- **Einstellungen**: Reparatur (Container-Restart fuer Brain/Bridge/Qdrant), Komplett-Reset, Betriebsmodi, Sprachausgabe + Voice-Cloning + F5-TTS-Tuning + Voice Export/Import, Whisper, Sprachmodell (brainModel), Onboarding-QR, App-Cleanup - **Einstellungen**: Reparatur (Container-Restart fuer Brain/Bridge/Qdrant), Komplett-Reset, Betriebsmodi, Sprachausgabe + Voice-Cloning + F5-TTS-Tuning + Voice Export/Import, Whisper, Sprachmodell (brainModel), Onboarding-QR, App-Cleanup
@@ -357,6 +364,9 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
- **Play-Button**: Jede ARIA-Nachricht kann nochmal vorgelesen werden (aus Cache wenn vorhanden, sonst neu rendern) - **Play-Button**: Jede ARIA-Nachricht kann nochmal vorgelesen werden (aus Cache wenn vorhanden, sonst neu rendern)
- **Chat-Suche**: Lupe in der Statusleiste filtert Nachrichten live - **Chat-Suche**: Lupe in der Statusleiste filtert Nachrichten live
- **Mülltonne pro Bubble** (mit Confirm): gezielt eine Nachricht loeschen — geht nicht nur aus der UI weg, sondern auch aus `chat_backup.jsonl`, Brain-Conversation-Window und allen anderen Clients (RVS-Broadcast). Wichtig damit ARIA den Turn auch beim naechsten Prompt nicht mehr im Kontext hat - **Mülltonne pro Bubble** (mit Confirm): gezielt eine Nachricht loeschen — geht nicht nur aus der UI weg, sondern auch aus `chat_backup.jsonl`, Brain-Conversation-Window und allen anderen Clients (RVS-Broadcast). Wichtig damit ARIA den Turn auch beim naechsten Prompt nicht mehr im Kontext hat
- **🗂️ Notizen-Inbox + Memory-Editor**: Neben der Lupe oeffnet `🗂️` ein Vollbild-Modal mit allen Memory/Trigger/Skill-Spezial-Bubbles aus dem Chat plus dem vollen DB-Browser. Tap auf eine Memory oeffnet ein **Detail/Edit-Modal**: Felder editieren, Anhaenge hoch-/runterladen + loeschen, Memory komplett loeschen. Identischer Editor auch in Settings → 🧠 Gedaechtnis. Spezial-Bubbles werden aus dem Chat-Stream gefiltert (keine ewig-unten-haengenden Notiz-Bubbles mehr)
- **Bubble-Header dynamic**: „ARIA hat etwas gemerkt" / „Notiz geaendert" (gelb) / „Notiz geloescht" (rot) — je nach action im memory_saved-Event
- **App-Crash-Reporting**: ungefangene JS-Errors + React-Render-Fehler landen automatisch in `/shared/logs/app.log` via RVS — kein ADB noetig, Logs holen via `tools/fetch-app-logs.sh` oder Diagnostic GET `/api/app-log`. ErrorBoundary verhindert White-Screen, zeigt stattdessen Error-Box im Modal mit Stack-Trace + Schliessen-Button
- **Mehrere Anhaenge**: Bilder + Dateien sammeln, Text hinzufuegen, dann zusammen senden - **Mehrere Anhaenge**: Bilder + Dateien sammeln, Text hinzufuegen, dann zusammen senden
- **Paste-Support**: Bilder aus Zwischenablage einfuegen (Diagnostic) - **Paste-Support**: Bilder aus Zwischenablage einfuegen (Diagnostic)
- **Anhaenge**: Bridge speichert in Shared Volume, ARIA kann darauf zugreifen, Re-Download ueber RVS - **Anhaenge**: Bridge speichert in Shared Volume, ARIA kann darauf zugreifen, Re-Download ueber RVS
@@ -867,10 +877,12 @@ docker exec aria-brain curl localhost:8080/memory/stats
- [x] **Phase B Punkt 2:** Migration aus `aria-data/brain-import/` → atomare Memory-Punkte (Identity / Rule / Preference / Tool / Skill, idempotent ueber migration_key) + Bootstrap-Snapshot Export/Import (nur pinned) - [x] **Phase B Punkt 2:** Migration aus `aria-data/brain-import/` → atomare Memory-Punkte (Identity / Rule / Preference / Tool / Skill, idempotent ueber migration_key) + Bootstrap-Snapshot Export/Import (nur pinned)
- [x] **Phase B Punkt 3:** Brain Conversation-Loop (Single-Chat UI, Rolling Window 50 Turns, Schwelle 60 → automatisches Destillat, manueller Trigger) - [x] **Phase B Punkt 3:** Brain Conversation-Loop (Single-Chat UI, Rolling Window 50 Turns, Schwelle 60 → automatisches Destillat, manueller Trigger)
- [x] **Phase B Punkt 4:** Skills-System (Python-only via local-venv, skill_create als Tool, dynamische run_<skill> Tools, Diagnostic Skills-Tab mit Logs/Toggle/Export/Import, skill_created Live-Notification in App+Diagnostic, harte Schwelle "pip → Skill") - [x] **Phase B Punkt 4:** Skills-System (Python-only via local-venv, skill_create als Tool, dynamische run_<skill> Tools, Diagnostic Skills-Tab mit Logs/Toggle/Export/Import, skill_created Live-Notification in App+Diagnostic, harte Schwelle "pip → Skill")
- [x] **Phase B Punkt 5:** Triggers-System (passive Aufweck-Quellen — Timer + Watcher mit safe Condition-Parser, GPS-near(), Diagnostic Trigger-Tab, kontinuierliches GPS-Tracking in der App fuer Use-Cases wie Blitzer-Warner). Inklusive Brain → Bridge HTTP-Push (Port 8090 intern) damit Trigger-Antworten ueber RVS in App + Diagnostic + TTS landen. - [x] **Phase B Punkt 5:** Triggers-System (passive Aufweck-Quellen — Timer + Watcher mit safe Condition-Parser, drei GPS-Funktionen `near()` / `entered_near()` / `left_near()` für unterschiedliche Geofencing-Modi, Diagnostic Trigger-Tab, kontinuierliches GPS-Tracking in der App fuer Use-Cases wie Blitzer-Warner). Tick-Frequenz 8s + event-getriebene Auswertung bei jedem `location_update` (statt 30s-Polling) damit auch Auto-Vorbeifahrten bei 100+ km/h durch kleine Radien zuverlässig erwischt werden. `near()`-Funktionen ignorieren GPS-Daten älter als 5 Minuten. Inklusive Brain → Bridge HTTP-Push (Port 8090 intern) damit Trigger-Antworten ueber RVS in App + Diagnostic + TTS landen.
- [x] **Proxy Tool-Use durchreichen**: claude-max-api-proxy patcht via eigene Adapter (`proxy-patches/`) den `tools`/`tool_calls`-Roundtrip — Claude Code rief vorher ihre internen Tools (Bash, sleep) statt der ARIA-Brain-Tools (trigger_timer, skill_*, ...). Jetzt funktioniert Tool-Use End-to-End. - [x] **Proxy Tool-Use durchreichen**: claude-max-api-proxy patcht via eigene Adapter (`proxy-patches/`) den `tools`/`tool_calls`-Roundtrip — Claude Code rief vorher ihre internen Tools (Bash, sleep) statt der ARIA-Brain-Tools (trigger_timer, skill_*, ...). Jetzt funktioniert Tool-Use End-to-End.
- [x] **Single Source of Truth — Qdrant**: `memory_save`-Tool fuer ARIA, Claude-Code-Auto-Memory abgeklemmt (tmpfs ueber `~/.claude/projects` im Proxy-Container), `brain-import/` zum reinen Drop-Folder degradiert, Cold-Memory mit Score-Threshold (0.30) gegen Embedder-Noise/Crosstalk, Diagnostic-Gehirn-UI mit Wortlich-/Semantisch-Suche, Advanced Search (AND/OR mit + Button), Memory-Druckansicht, Muelltonne pro Chat-Bubble. DB ist jetzt durchgaengig die einzige Wissensquelle, kein paralleles File-Memory mehr. - [x] **Single Source of Truth — Qdrant**: `memory_save`-Tool fuer ARIA, Claude-Code-Auto-Memory abgeklemmt (tmpfs ueber `~/.claude/projects` im Proxy-Container), `brain-import/` zum reinen Drop-Folder degradiert, Cold-Memory mit Score-Threshold (0.30) gegen Embedder-Noise/Crosstalk, Diagnostic-Gehirn-UI mit Wortlich-/Semantisch-Suche, Advanced Search (AND/OR mit + Button), Memory-Druckansicht, Muelltonne pro Chat-Bubble. DB ist jetzt durchgaengig die einzige Wissensquelle, kein paralleles File-Memory mehr.
- [x] **Memory-Anhaenge mit Vision-Pipeline**: Pro Memory koennen Bilder/PDFs/beliebige Dateien angehaengt werden (unter `/shared/memory-attachments/<id>/`, max 20 MB). Diagnostic-UI mit Thumbnail-Vorschau + Lightbox, App `memory_saved`-Bubble mit Tap-to-Load via RVS, System-Prompt zeigt Anhang-Pfade. **ARIA sieht Bilder echt** via Claude Code's eingebautes multi-modales `Read`-Tool — kein Proxy-Patch noetig. `memory_save` hat `attach_paths`-Parameter sodass ARIA ein User-Foto im selben Tool-Call lesen, Infos extrahieren (Kennzeichen, Marken, Texte) und als Memory + Anhang persistieren kann. Bilder bleiben am Memory haengen — bei spaeteren Detail-Fragen liest ARIA das Bild einfach nochmal. - [x] **Memory-Anhaenge mit Vision-Pipeline**: Pro Memory koennen Bilder/PDFs/beliebige Dateien angehaengt werden (unter `/shared/memory-attachments/<id>/`, max 20 MB). Diagnostic-UI mit Thumbnail-Vorschau + Lightbox, App `memory_saved`-Bubble mit Tap-to-Load via RVS, System-Prompt zeigt Anhang-Pfade. **ARIA sieht Bilder echt** via Claude Code's eingebautes multi-modales `Read`-Tool — kein Proxy-Patch noetig. `memory_save` hat `attach_paths`-Parameter sodass ARIA ein User-Foto im selben Tool-Call lesen, Infos extrahieren (Kennzeichen, Marken, Texte) und als Memory + Anhang persistieren kann. Bilder bleiben am Memory haengen — bei spaeteren Detail-Fragen liest ARIA das Bild einfach nochmal.
- [x] **Memory-Editor in der App** (5 Etappen): Notizen-Inbox-Button neben der Lupe oeffnet ein Modal mit allen Spezial-Bubbles aus dem aktuellen Chat plus dem vollen DB-Browser. Tap auf eine Memory → Detail-Modal mit Anhang-Vorschau, Stift-Icon wechselt in Edit-Mode (Felder editieren + Anhaenge hoch-/runterladen + loeschen). Identischer Editor unter Settings → 🧠 Gedaechtnis. Bubble-Header dynamic je nach Aktion (created/updated/deleted). RVS-Brain-Proxy als Fundament (`brain_request`/`brain_response`) damit die App beliebige Brain-HTTP-Endpoints adressieren kann. `memory_search` + `memory_update` als ARIA-Tools damit sie aktiv die DB pruefen und Eintraege patchen kann statt zu fragmentieren.
- [x] **App-Crash-Reporting via RVS**: ErrorBoundary + global JS-Error-Handler + Promise-Rejection-Tracker schicken Crashes als `app_log`-Event durch RVS. Bridge sammelt in `/shared/logs/app.log`, Diagnostic GET `/api/app-log`. `tools/fetch-app-logs.sh` holt die Logs auf die Dev-Maschine (gitignored `.aria-debug/`). Damit kann Stefan unterwegs ohne ADB debuggen — der erste Bug (URLSearchParams in Hermes) wurde so in 5 Minuten gefunden.
- [x] Sprachmodell-Setting wieder funktional (brainModel in runtime.json statt aria-core) - [x] Sprachmodell-Setting wieder funktional (brainModel in runtime.json statt aria-core)
- [x] App-Chat-Sync: kompletter Server-Sync bei Reconnect (Server = Source of Truth) + chat_cleared Live-Update. Lokal-only Bubbles (Skill-Notifications, laufende Voice ohne STT) bleiben erhalten. - [x] App-Chat-Sync: kompletter Server-Sync bei Reconnect (Server = Source of Truth) + chat_cleared Live-Update. Lokal-only Bubbles (Skill-Notifications, laufende Voice ohne STT) bleiben erhalten.
- [x] App: Chat-Suche mit Next/Prev Navigation statt Filter - [x] App: Chat-Suche mit Next/Prev Navigation statt Filter
+4 -1
View File
@@ -13,7 +13,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import ChatScreen from './src/screens/ChatScreen'; import ChatScreen from './src/screens/ChatScreen';
import SettingsScreen from './src/screens/SettingsScreen'; import SettingsScreen from './src/screens/SettingsScreen';
import rvs from './src/services/rvs'; import rvs from './src/services/rvs';
import { initLogger } from './src/services/logger'; import { initLogger, installGlobalCrashReporter } from './src/services/logger';
// --- Navigation --- // --- Navigation ---
@@ -49,6 +49,9 @@ const App: React.FC = () => {
// initLogger ist async aber blockt nichts — solange er noch laueft, // initLogger ist async aber blockt nichts — solange er noch laueft,
// loggen wir normal (Default an), danach respektiert console.log das Setting. // loggen wir normal (Default an), danach respektiert console.log das Setting.
initLogger().catch(() => {}); initLogger().catch(() => {});
// Crash-Reporter installieren — ungefangene JS-Errors landen via RVS
// bei der Bridge (sichtbar in /shared/logs/app.log + Diagnostic-API)
installGlobalCrashReporter();
const initConnection = async () => { const initConnection = async () => {
const config = await rvs.loadConfig(); const config = await rvs.loadConfig();
if (config) { if (config) {
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit" applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10302 versionCode 10402
versionName "0.1.3.2" versionName "0.1.4.2"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.3.2", "version": "0.1.4.2",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+89
View File
@@ -0,0 +1,89 @@
/**
* ErrorBoundary — fängt React-Render-Fehler und zeigt eine Error-Box
* statt White-Screen-of-Death. Plus: Crash wird zum logger geschickt,
* der das ueber RVS an die Bridge weiterleitet.
*
* Einsatz: kritische Komponenten/Modals damit ein Bug nicht die ganze
* App killt.
*/
import React from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { reportAppError } from '../services/logger';
interface Props {
children: React.ReactNode;
/** Optional: Bezeichnung der eingegrenzten Section fuer's Log. */
scope?: string;
/** Optional: Reset-Callback (z.B. Modal schliessen) — Button ist dann sichtbar. */
onReset?: () => void;
}
interface State {
err: Error | null;
info: string;
}
export class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { err: null, info: '' };
}
static getDerivedStateFromError(err: Error): Partial<State> {
return { err };
}
componentDidCatch(err: Error, info: any) {
const stack = info?.componentStack || '';
this.setState({ info: stack });
reportAppError({
scope: this.props.scope || 'ErrorBoundary',
message: err?.message || String(err),
stack: (err?.stack || '') + '\n--- componentStack ---\n' + stack,
});
}
render() {
if (this.state.err) {
return (
<View style={s.box}>
<Text style={s.title}> Etwas ist schiefgegangen</Text>
<Text style={s.scope}>{this.props.scope || 'unbekannte Komponente'}</Text>
<ScrollView style={s.scroll}>
<Text style={s.msg}>{this.state.err.message || String(this.state.err)}</Text>
{this.state.info ? <Text style={s.stack}>{this.state.info}</Text> : null}
</ScrollView>
{this.props.onReset ? (
<TouchableOpacity style={s.btn} onPress={() => { this.setState({err:null,info:''}); this.props.onReset?.(); }}>
<Text style={s.btnText}>Schliessen + zurueck</Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={s.btn} onPress={() => this.setState({err:null,info:''})}>
<Text style={s.btnText}>Erneut versuchen</Text>
</TouchableOpacity>
)}
<Text style={s.hint}>
Crash wurde an die Bridge gemeldet sichtbar in der Diagnostic-Web-UI unter /api/app-log
</Text>
</View>
);
}
return this.props.children;
}
}
const s = StyleSheet.create({
box: { flex:1, padding:16, backgroundColor:'#1A0A0A' },
title: { color:'#FF6B6B', fontWeight:'bold', fontSize:16, marginBottom:6 },
scope: { color:'#FF9500', fontSize:12, marginBottom:10 },
scroll: { flex:1, backgroundColor:'#0D0D1A', borderRadius:6, padding:10, marginBottom:10 },
msg: { color:'#FF6B6B', fontSize:13, marginBottom:8 },
stack: { color:'#8888AA', fontSize:11, fontFamily:'monospace' },
btn: { backgroundColor:'#0096FF', paddingVertical:10, borderRadius:6, alignItems:'center' },
btnText: { color:'#fff', fontWeight:'600' },
hint: { color:'#555570', fontSize:10, marginTop:8, textAlign:'center' },
});
export default ErrorBoundary;
+68 -33
View File
@@ -37,9 +37,11 @@ interface Props {
title?: string; title?: string;
/** Style-Erweiterung fuer den Container. */ /** Style-Erweiterung fuer den Container. */
flatStyle?: boolean; flatStyle?: boolean;
/** Wenn gesetzt: kein eigenes DetailModal mounten — Parent kuemmert sich. */
onOpenMemory?: (id: string) => void;
} }
export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle }) => { export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle, onOpenMemory }) => {
const [items, setItems] = useState<Memory[]>([]); const [items, setItems] = useState<Memory[]>([]);
const [filtered, setFiltered] = useState<Memory[]>([]); const [filtered, setFiltered] = useState<Memory[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -82,38 +84,35 @@ export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle
setFiltered(out); setFiltered(out);
}, [items, q, typeFilter, pinnedFilter, restrictToIds]); }, [items, q, typeFilter, pinnedFilter, restrictToIds]);
const [showNewMemoryDialog, setShowNewMemoryDialog] = useState(false);
const [newMemoryTitle, setNewMemoryTitle] = useState('');
const onAddNew = () => { const onAddNew = () => {
Alert.prompt( setNewMemoryTitle('');
'Neue Memory', setShowNewMemoryDialog(true);
'Titel:', };
[
{ text: 'Abbrechen', style: 'cancel' }, const confirmAddNew = async () => {
{ const t = newMemoryTitle.trim();
text: 'Anlegen', if (!t) { setShowNewMemoryDialog(false); return; }
onPress: async (title?: string) => { setShowNewMemoryDialog(false);
const t = (title || '').trim(); try {
if (!t) return; const m = await brainApi.saveMemory({
try { type: 'fact', title: t,
const m = await brainApi.saveMemory({ content: '(noch leer — bitte editieren)',
type: 'fact', title: t, });
content: '(noch leer — bitte editieren)', load();
}); if (onOpenMemory) onOpenMemory(m.id);
load(); else setOpenId(m.id);
setOpenId(m.id); } catch (e: any) {
} catch (e: any) { Alert.alert('Fehler', String(e?.message || e));
Alert.alert('Fehler', String(e?.message || e)); }
}
},
},
],
'plain-text',
);
}; };
const renderItem = ({ item }: { item: Memory }) => { const renderItem = ({ item }: { item: Memory }) => {
const attCount = (item.attachments || []).length; const attCount = (item.attachments || []).length;
return ( return (
<TouchableOpacity style={s.row} onPress={() => setOpenId(item.id)}> <TouchableOpacity style={s.row} onPress={() => onOpenMemory ? onOpenMemory(item.id) : setOpenId(item.id)}>
<View style={{flex:1}}> <View style={{flex:1}}>
<Text style={s.rowTitle} numberOfLines={1}> <Text style={s.rowTitle} numberOfLines={1}>
{item.pinned ? '📌 ' : ''}{item.title || '(ohne Titel)'} {item.pinned ? '📌 ' : ''}{item.title || '(ohne Titel)'}
@@ -170,6 +169,12 @@ export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle
data={filtered} data={filtered}
keyExtractor={m => m.id} keyExtractor={m => m.id}
renderItem={renderItem} renderItem={renderItem}
// nestedScrollEnabled: notwendig damit die FlatList auf Android
// scrollt wenn sie in einer aeusseren ScrollView haengt (Settings-
// Screen ist ScrollView). Ohne das frisst der aeussere ScrollView
// alle Gesten und die innere Liste ist tot.
nestedScrollEnabled={true}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={ ListEmptyComponent={
<Text style={{color:'#555570',textAlign:'center',padding:20,fontStyle:'italic'}}> <Text style={{color:'#555570',textAlign:'center',padding:20,fontStyle:'italic'}}>
{items.length === 0 ? '(keine Memories in der DB)' : '(keine Treffer für diese Filter)'} {items.length === 0 ? '(keine Memories in der DB)' : '(keine Treffer für diese Filter)'}
@@ -202,12 +207,42 @@ export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle
</TouchableOpacity> </TouchableOpacity>
</Modal> </Modal>
<MemoryDetailModal {/* Eigenes DetailModal nur wenn der Parent kein Callback uebergibt
memoryId={openId} (vermeidet Modal-in-Modal-Stacking auf Android). */}
visible={!!openId} {!onOpenMemory && (
onClose={() => { setOpenId(null); load(); }} <MemoryDetailModal
onDeleted={() => { setOpenId(null); load(); }} memoryId={openId}
/> visible={!!openId}
onClose={() => { setOpenId(null); load(); }}
onDeleted={() => { setOpenId(null); load(); }}
/>
)}
{/* "Neue Memory"-Dialog (Alert.prompt ist iOS-only, daher eigenes Modal) */}
<Modal visible={showNewMemoryDialog} transparent animationType="fade" onRequestClose={() => setShowNewMemoryDialog(false)}>
<View style={s.menuBack}>
<View style={[s.menuBox, {padding:16, minWidth:280}]}>
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:14, marginBottom:10}}>Neue Memory anlegen</Text>
<Text style={{color:'#8888AA', fontSize:11, marginBottom:6}}>Titel:</Text>
<TextInput
value={newMemoryTitle}
onChangeText={setNewMemoryTitle}
autoFocus
placeholder="z.B. Stefans Auto"
placeholderTextColor="#555570"
style={{backgroundColor:'#1E1E2E', color:'#E0E0F0', padding:8, borderRadius:4, fontSize:13, marginBottom:12}}
/>
<View style={{flexDirection:'row', gap:8, justifyContent:'flex-end'}}>
<TouchableOpacity onPress={() => setShowNewMemoryDialog(false)} style={{padding:8}}>
<Text style={{color:'#8888AA'}}>Abbrechen</Text>
</TouchableOpacity>
<TouchableOpacity onPress={confirmAddNew} style={{backgroundColor:'#0096FF', paddingHorizontal:14, paddingVertical:8, borderRadius:4}}>
<Text style={{color:'#fff', fontWeight:'600'}}>Anlegen</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View> </View>
); );
}; };
+118 -31
View File
@@ -30,6 +30,7 @@ import { Dimensions } from 'react-native';
import ZoomableImage from '../components/ZoomableImage'; import ZoomableImage from '../components/ZoomableImage';
import MemoryDetailModal from '../components/MemoryDetailModal'; import MemoryDetailModal from '../components/MemoryDetailModal';
import MemoryBrowser from '../components/MemoryBrowser'; import MemoryBrowser from '../components/MemoryBrowser';
import ErrorBoundary from '../components/ErrorBoundary';
import rvs, { RVSMessage, ConnectionState } from '../services/rvs'; import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
import audioService from '../services/audio'; import audioService from '../services/audio';
import wakeWordService from '../services/wakeword'; import wakeWordService from '../services/wakeword';
@@ -235,6 +236,7 @@ const ChatScreen: React.FC = () => {
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null); const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null); const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
const [inboxVisible, setInboxVisible] = useState(false); const [inboxVisible, setInboxVisible] = useState(false);
const [showJumpDown, setShowJumpDown] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchVisible, setSearchVisible] = useState(false); const [searchVisible, setSearchVisible] = useState(false);
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
@@ -1051,9 +1053,10 @@ const ChatScreen: React.FC = () => {
}, [searchQuery]); }, [searchQuery]);
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen. // Bei Index-Wechsel zu der entsprechenden Bubble scrollen.
// FlatList ist `inverted` viewPosition 0.5 (mitte) ist beim inverted-Render // FlatList ist `inverted`. viewPosition 0 = Item-Top oben am Viewport →
// tatsaechlich die Mitte des sichtbaren Bereichs. Wir verzoegern minimal // Treffer-Bubble liegt mit dem Anfang direkt oben sichtbar, kein
// damit Layout sicher fertig ist. // weiteres Hochscrollen noetig. Plus mehrere Retries da Layout bei
// langen Listen zeitversetzt fertig wird.
useEffect(() => { useEffect(() => {
if (!searchMatchIds.length) return; if (!searchMatchIds.length) return;
const id = searchMatchIds[searchIndex]; const id = searchMatchIds[searchIndex];
@@ -1062,13 +1065,16 @@ const ChatScreen: React.FC = () => {
if (idx < 0 || !flatListRef.current) return; if (idx < 0 || !flatListRef.current) return;
const tryScroll = () => { const tryScroll = () => {
try { try {
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 }); flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0 });
} catch { } catch {
// wird von onScrollToIndexFailed nochmal versucht // wird von onScrollToIndexFailed nochmal versucht
} }
}; };
// requestAnimationFrame statt setTimeout 0 — wartet auf naechsten Layout-Frame // requestAnimationFrame fuer den ersten Versuch, dann setTimeout-Folge
// damit auch bei tiefen Indizes (viel ungelayoutete Items dazwischen)
// der Sprung am Ende sitzt.
requestAnimationFrame(tryScroll); requestAnimationFrame(tryScroll);
[180, 420, 800].forEach(d => setTimeout(tryScroll, d));
}, [searchIndex, searchMatchIds, invertedMessages]); }, [searchIndex, searchMatchIds, invertedMessages]);
const activeSearchId = searchMatchIds[searchIndex] || ''; const activeSearchId = searchMatchIds[searchIndex] || '';
@@ -1374,17 +1380,30 @@ const ChatScreen: React.FC = () => {
<TouchableOpacity <TouchableOpacity
key={`${item.id}-att-${idx}`} key={`${item.id}-att-${idx}`}
style={styles.memoryAttachmentRow} style={styles.memoryAttachmentRow}
onPress={() => { onPress={async () => {
if (!a.path) return; if (!a.path) return;
if (a.localUri) { if (a.localUri) {
if (isImage) setFullscreenImage(a.localUri); const localPath = a.localUri.replace(/^file:\/\//, '');
else openFileWithIntent(a.localUri.replace(/^file:\/\//, ''), a.mime || ''); const exists = await RNFS.exists(localPath).catch(() => false);
} else { if (exists) {
// Datei via Bridge nachladen — file_response hat den if (isImage) setFullscreenImage(a.localUri);
// memorySaved-Match-Path und cached + zeigt direkt else openFileWithIntent(localPath, a.mime || '');
autoOpenPaths.current.add(a.path); return;
rvs.send('file_request' as any, { serverPath: a.path, requestId: `memAtt_${item.id}_${idx}` }); }
// Cache weg → localUri leeren + neu laden
setMessages(prev => prev.map(mm => mm.id === item.id && mm.memorySaved
? { ...mm, memorySaved: { ...mm.memorySaved,
attachments: mm.memorySaved.attachments?.map(x =>
x.path === a.path ? { ...x, localUri: undefined } : x) } }
: mm));
if (Platform.OS === 'android') {
ToastAndroid.show('Cache leer — lade nach...', ToastAndroid.SHORT);
}
} }
// Datei via Bridge nachladen — file_response hat den
// memorySaved-Match-Path und cached + zeigt direkt
autoOpenPaths.current.add(a.path);
rvs.send('file_request' as any, { serverPath: a.path, requestId: `memAtt_${item.id}_${idx}` });
}} }}
> >
<Text style={styles.memoryAttachmentIcon}>{icon}</Text> <Text style={styles.memoryAttachmentIcon}>{icon}</Text>
@@ -1488,17 +1507,32 @@ const ChatScreen: React.FC = () => {
) : ( ) : (
<TouchableOpacity <TouchableOpacity
style={styles.attachmentFile} style={styles.attachmentFile}
onPress={() => { onPress={async () => {
// Lokal vorhanden \u2192 direkt mit System-Intent oeffnen // Lokal vorhanden? Cache koennte geleert worden sein \u2014
// Datei-Existenz pruefen bevor wir den Intent feuern.
if (att.uri) { if (att.uri) {
openFileWithIntent(att.uri.replace(/^file:\/\//, ''), att.mimeType || ''); const localPath = att.uri.replace(/^file:\/\//, '');
return; const exists = await RNFS.exists(localPath).catch(() => false);
if (exists) {
openFileWithIntent(localPath, att.mimeType || '');
return;
}
// Cache weg \u2192 uri im State leeren damit UI "tippen zum Laden" zeigt
setMessages(prev => prev.map(m => m.id === item.id
? { ...m, attachments: m.attachments?.map(a =>
a.serverPath === att.serverPath ? { ...a, uri: undefined } : a) }
: m));
if (Platform.OS === 'android') {
ToastAndroid.show('Cache leer \u2014 lade nach...', ToastAndroid.SHORT);
}
} }
// Sonst: file_request \u2192 bei file_response wird die Datei // Re-Download via file_request \u2192 bei file_response wird die
// gespeichert UND geoeffnet (autoOpenPaths-Tracking). // Datei gespeichert UND geoeffnet (autoOpenPaths-Tracking).
if (att.serverPath) { if (att.serverPath) {
autoOpenPaths.current.add(att.serverPath); autoOpenPaths.current.add(att.serverPath);
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id }); 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);
} }
}} }}
> >
@@ -1697,15 +1731,27 @@ const ChatScreen: React.FC = () => {
ref={flatListRef} ref={flatListRef}
inverted inverted
data={invertedMessages} data={invertedMessages}
onScroll={(e) => {
// Bei inverted FlatList: contentOffset.y > 0 = weg von "unten"
// (= aelter scrollen). Wir zeigen den Jump-Down-Button ab ~250px.
const y = e.nativeEvent.contentOffset.y;
setShowJumpDown(y > 250);
}}
scrollEventThrottle={120}
onScrollToIndexFailed={(info) => { onScrollToIndexFailed={(info) => {
// FlatList kennt das Item-Layout noch nicht. Zuerst grob in die // FlatList kennt das Item-Layout noch nicht. Zuerst grob in die
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann nach 250ms // Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann mehrfach
// praezise nochmal versuchen. // praezise nachsetzen — bei langem Chat braucht's manchmal mehrere
// Runden bis die Layouts gemessen sind.
const offset = info.averageItemLength * info.index; const offset = info.averageItemLength * info.index;
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {} try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
setTimeout(() => { // viewPosition 0 = Item-Top oben am Viewport → Stefan landet am
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); } catch {} // Text-Anfang der Bubble, nicht in der Mitte oder am Ende.
}, 250); [120, 320, 600].forEach(delay => {
setTimeout(() => {
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}
}, delay);
});
}} }}
keyExtractor={item => item.id} keyExtractor={item => item.id}
renderItem={renderMessage} renderItem={renderMessage}
@@ -1772,6 +1818,24 @@ const ChatScreen: React.FC = () => {
</View> </View>
)} )}
{/* Jump-to-Bottom-Button — erscheint wenn man weg von der neuesten
Nachricht gescrollt hat. Bei inverted FlatList ist scrollToOffset
0 == neueste Nachricht visuell unten. */}
{showJumpDown && (
<TouchableOpacity
style={styles.jumpDownBtn}
activeOpacity={0.85}
onPress={() => {
try {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
} catch {}
setShowJumpDown(false);
}}
>
<Text style={{color:'#fff', fontSize:18, fontWeight:'700'}}>{'↓'}</Text>
</TouchableOpacity>
)}
{/* Eingabebereich */} {/* Eingabebereich */}
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
{/* Datei-Buttons */} {/* Datei-Buttons */}
@@ -1839,17 +1903,22 @@ const ChatScreen: React.FC = () => {
</View> </View>
{/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */} {/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */}
<MemoryDetailModal {memoryDetailId ? (
memoryId={memoryDetailId} <ErrorBoundary scope="ChatScreen.MemoryDetailModal" onReset={() => setMemoryDetailId(null)}>
visible={!!memoryDetailId} <MemoryDetailModal
onClose={() => setMemoryDetailId(null)} memoryId={memoryDetailId}
onDeleted={() => setMemoryDetailId(null)} visible={!!memoryDetailId}
/> onClose={() => setMemoryDetailId(null)}
onDeleted={() => setMemoryDetailId(null)}
/>
</ErrorBoundary>
) : null}
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles). {/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-Bubbles Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-Bubbles
des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */} des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */}
<Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}> <Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}>
<ErrorBoundary scope="ChatScreen.InboxModal" onReset={() => setInboxVisible(false)}>
<View style={{flex:1, backgroundColor:'#0D0D1A'}}> <View style={{flex:1, backgroundColor:'#0D0D1A'}}>
<View style={{flexDirection:'row', alignItems:'center', padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}> <View style={{flexDirection:'row', alignItems:'center', padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:16, flex:1}}>{'🗂️'} Notizen-Inbox</Text> <Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:16, flex:1}}>{'🗂️'} Notizen-Inbox</Text>
@@ -1933,8 +2002,9 @@ const ChatScreen: React.FC = () => {
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:10, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}> <Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:10, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
Alle Memories aus der DB Alle Memories aus der DB
</Text> </Text>
<MemoryBrowser /> <MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
</View> </View>
</ErrorBoundary>
</Modal> </Modal>
{/* Bild-Vollbild Modal */} {/* Bild-Vollbild Modal */}
@@ -2306,6 +2376,23 @@ const styles = StyleSheet.create({
color: '#555570', color: '#555570',
fontSize: 10, fontSize: 10,
}, },
jumpDownBtn: {
position: 'absolute',
right: 16,
bottom: 80,
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#0096FF',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.4,
shadowRadius: 4,
elevation: 5,
zIndex: 100,
},
bubbleTrash: { bubbleTrash: {
position: 'absolute', position: 'absolute',
top: 4, top: 4,
+1 -1
View File
@@ -868,7 +868,7 @@ const SettingsScreen: React.FC = () => {
})()} })()}
</View> </View>
</Modal> </Modal>
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content} nestedScrollEnabled={true}>
{currentSection === null && ( {currentSection === null && (
<> <>
+29 -15
View File
@@ -54,6 +54,18 @@ function _newRequestId(): string {
return `brain_${Date.now().toString(36)}_${_nextId}`; return `brain_${Date.now().toString(36)}_${_nextId}`;
} }
/** Mini-Query-String-Builder ohne URLSearchParams (Hermes-Polyfill kennt
* kein URLSearchParams.set, crasht). Akzeptiert object mit string/number/
* bool-Values; undefined/null/leere Strings werden ausgelassen. */
function _qs(params: Record<string, unknown>): string {
const parts: string[] = [];
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null || v === '') continue;
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
}
return parts.length ? `?${parts.join('&')}` : '';
}
interface SendOpts { interface SendOpts {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
body?: AnyJson; body?: AnyJson;
@@ -119,29 +131,31 @@ export const brainApi = {
/** Liste aller Memories, optional nach Type gefiltert. */ /** Liste aller Memories, optional nach Type gefiltert. */
listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> { listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> {
const qs = new URLSearchParams(); const qs = _qs({ type: opts.type, limit: opts.limit || 500 });
if (opts.type) qs.set('type', opts.type); return _send(`/memory/list${qs}`);
qs.set('limit', String(opts.limit || 500));
return _send(`/memory/list?${qs.toString()}`);
}, },
/** Volltext-Substring-Suche. */ /** Volltext-Substring-Suche. */
searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> { searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> {
const qs = new URLSearchParams({ q }); const qs = _qs({
if (opts.type) qs.set('type', opts.type); q,
qs.set('include_pinned', String(opts.includePinned !== false)); type: opts.type,
qs.set('k', String(opts.k || 50)); include_pinned: opts.includePinned !== false,
return _send(`/memory/search-text?${qs.toString()}`); k: opts.k || 50,
});
return _send(`/memory/search-text${qs}`);
}, },
/** Semantische Suche (Embedder). */ /** Semantische Suche (Embedder). */
searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> { searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> {
const qs = new URLSearchParams({ q }); const qs = _qs({
if (opts.type) qs.set('type', opts.type); q,
qs.set('include_pinned', String(opts.includePinned !== false)); type: opts.type,
qs.set('k', String(opts.k || 10)); include_pinned: opts.includePinned !== false,
qs.set('score_threshold', String(opts.threshold ?? 0.30)); k: opts.k || 10,
return _send(`/memory/search?${qs.toString()}`); score_threshold: opts.threshold ?? 0.30,
});
return _send(`/memory/search${qs}`);
}, },
/** Memory anlegen. */ /** Memory anlegen. */
+76
View File
@@ -7,6 +7,8 @@
*/ */
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
import rvs from './rvs';
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging'; export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
@@ -39,3 +41,77 @@ export function setVerboseLogging(verbose: boolean): void {
applyState(); applyState();
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {}); AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
} }
// ─── App-Crash-Reporting via RVS ────────────────────────────────────
//
// Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder
// ungefangener JS-Error (ErrorUtils-Handler) — schicken wir den Crash
// als RVS-Message vom Typ "app_log" an die Bridge. Die schreibt in
// /shared/logs/app.log, sodass wir/Diagnostic die Crashes mitlesen
// koennen ohne ADB.
interface AppErrorEvent {
scope: string;
message: string;
stack?: string;
level?: 'error' | 'warn' | 'info';
}
let _reportingInstalled = false;
/** Schickt einen App-Fehler via RVS an die Bridge. */
export function reportAppError(ev: AppErrorEvent): void {
try {
rvs.send('app_log' as any, {
ts: Date.now(),
platform: Platform.OS,
level: ev.level || 'error',
scope: ev.scope,
message: ev.message,
stack: (ev.stack || '').slice(0, 8000),
});
} catch {
// RVS noch nicht connected — Fehler geht im console weiter.
}
// Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar)
// den Crash sieht.
console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || '');
}
/** Installiert einen globalen JS-Error-Handler der ungefangene Errors via
* RVS an die Bridge schickt. Beim App-Start aufrufen. */
export function installGlobalCrashReporter(): void {
if (_reportingInstalled) return;
_reportingInstalled = true;
try {
const g: any = global as any;
const prev = g.ErrorUtils?.getGlobalHandler?.();
g.ErrorUtils?.setGlobalHandler?.((err: any, isFatal: boolean) => {
reportAppError({
scope: isFatal ? 'global-fatal' : 'global-nonfatal',
message: (err && err.message) || String(err),
stack: err && err.stack,
});
// Original-Handler weiterhin aufrufen damit React-Native das System-
// Crash-Overlay zeigt (im Dev-Build) bzw. in Production sauber stirbt.
if (typeof prev === 'function') {
try { prev(err, isFatal); } catch {}
}
});
// unhandled Promise-Rejections — manche RN-Versionen haben das nicht
// automatisch im ErrorUtils.
g.HermesInternal?.enablePromiseRejectionTracker?.({
allRejections: true,
onUnhandled: (id: number, err: any) => {
reportAppError({
scope: 'promise-unhandled',
level: 'warn',
message: (err && err.message) || String(err),
stack: err && err.stack,
});
},
});
} catch {
// ErrorUtils nicht da → nix machen
}
}
+11 -2
View File
@@ -134,10 +134,19 @@ META_TOOLS = [
"function": { "function": {
"name": "trigger_watcher", "name": "trigger_watcher",
"description": ( "description": (
"Lege einen Watcher-Trigger an — pollt alle paar Minuten eine Condition, " "Lege einen Watcher-Trigger an — pollt eine Condition, "
"feuert wenn sie wahr wird (mit Throttle damit's nicht spammt). " "feuert wenn sie wahr wird (mit Throttle damit's nicht spammt). "
"Use-Case: 'sag bescheid wenn Disk unter 5GB', 'pingt mich wenn um 8 Uhr'. " "Use-Case: 'sag bescheid wenn Disk unter 5GB', 'pingt mich wenn um 8 Uhr'. "
"Welche Variablen verfuegbar sind und ihre Bedeutung steht im System-Prompt." "Welche Variablen verfuegbar sind und ihre Bedeutung steht im System-Prompt.\n\n"
"Fuer GPS-Trigger gibt es DREI Modi — waehle nach Use-Case:\n"
"- **`near(lat, lon, r)`**: SOLANGE im Radius (mit Throttle gegen Spam). "
"Use-Case: 'bin ich noch in der Naehe von X?'. Empfohlener throttle 300-3600s.\n"
"- **`entered_near(lat, lon, r)`**: EINMAL beim Eintritt (Uebergang draussen→innen). "
"Use-Case: Blitzer-Warner, Ankunfts-Erinnerung. Mit grossem r (z.B. 2000) "
"wird's zur Vorwarnung 2 km vor dem Ziel. Empfohlener throttle: kurz (30-60s, "
"nur gegen GPS-Jitter).\n"
"- **`left_near(lat, lon, r)`**: EINMAL beim Verlassen (Uebergang innen→draussen). "
"Use-Case: 'Hast du am Parkplatz X was vergessen?'. Empfohlener throttle: kurz."
), ),
"parameters": { "parameters": {
"type": "object", "type": "object",
+68 -19
View File
@@ -27,7 +27,12 @@ import watcher as watcher_mod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TICK_SEC = 30 # Polling-Frequenz des Background-Loops. Vorher 30s → Auto-Vorbeifahrt
# durch einen 300m-Radius bei >50 km/h konnte zwischen zwei Ticks komplett
# verpasst werden. Mit 8s ist auch eine 18-Sekunden-Durchfahrt (120 km/h
# durch 300m) garantiert mind. einmal getroffen. Der Loop ist billig
# (paar Dateilesungen + AST-Eval), das macht Brain nicht warm.
TICK_SEC = 8
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090") BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
@@ -159,7 +164,12 @@ async def _fire(trigger: dict, agent_factory) -> None:
async def _tick(agent_factory) -> None: async def _tick(agent_factory) -> None:
"""Ein Pruefdurchlauf. Geht ueber alle Triggers, feuert was zu feuern ist.""" """Ein Pruefdurchlauf. Geht ueber alle Triggers, feuert was zu feuern ist.
near()-State-Tracking: entered_near/left_near brauchen die Information
ob ein near()-Aufruf beim letzten Tick true war (Uebergang erkennen).
Wir halten das pro Trigger als near_states-Dict im Manifest und
aktualisieren es nach jedem Eval — auch wenn nicht gefeuert wird."""
try: try:
all_triggers = triggers_mod.list_triggers(active_only=True) all_triggers = triggers_mod.list_triggers(active_only=True)
except Exception as e: except Exception as e:
@@ -168,35 +178,74 @@ async def _tick(agent_factory) -> None:
if not all_triggers: if not all_triggers:
return return
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
# Variablen einmal pro Tick sammeln (nicht pro Trigger — Disk-Stat ist teuer)
try:
vars_ = watcher_mod.collect_variables()
except Exception as e:
logger.warning("collect_variables: %s", e)
vars_ = {}
# Watcher: last_checked_at jetzt updaten (auch wenn nicht gefeuert wird,
# damit der Check-Interval respektiert wird)
for t in all_triggers:
if t.get("type") == "watcher":
try:
t["last_checked_at"] = _now_iso()
triggers_mod.write(t["name"], t)
except Exception:
pass
for trigger in all_triggers: for trigger in all_triggers:
if trigger.get("type") != "watcher":
continue
try: try:
if _should_fire(trigger, vars_, now): # Variablen pro Trigger sammeln — wegen prev_near_states-Closure
prev = trigger.get("near_states") or {}
vars_ = watcher_mod.collect_variables(prev_near_states=prev)
# Condition evaluieren via _should_fire (intern ruft watcher.evaluate)
fired = _should_fire(trigger, vars_, now)
# State immer updaten, egal ob gefeuert wurde — sonst greift
# entered_near/left_near nicht
new_states = vars_.get("_new_near_states") or {}
trigger["near_states"] = new_states
trigger["last_checked_at"] = _now_iso()
try:
triggers_mod.write(trigger["name"], trigger)
except Exception as e:
logger.warning("trigger.write %s: %s", trigger.get("name"), e)
if fired:
# Feuern als eigener Task — wenn ARIA langsam antwortet, # Feuern als eigener Task — wenn ARIA langsam antwortet,
# darf der naechste Tick nicht blockieren # darf der naechste Tick nicht blockieren
asyncio.create_task(_fire(trigger, agent_factory)) asyncio.create_task(_fire(trigger, agent_factory))
except Exception as e: except Exception as e:
logger.warning("Trigger-Check %s: %s", trigger.get("name"), e) logger.warning("Trigger-Check %s: %s", trigger.get("name"), e)
# Timer (one-shot) — separat ohne near-State
timer_vars = None
for trigger in all_triggers:
if trigger.get("type") != "timer":
continue
try:
if timer_vars is None:
timer_vars = watcher_mod.collect_variables()
if _should_fire(trigger, timer_vars, now):
asyncio.create_task(_fire(trigger, agent_factory))
except Exception as e:
logger.warning("Timer-Check %s: %s", trigger.get("name"), e)
# Module-Level-Slot fuer die agent_factory damit on-demand-Ticks (von
# z.B. POST /triggers/check-now) Zugang haben ohne durch den ganzen
# Lifespan-Pfad geschleust zu werden.
_AGENT_FACTORY = None
async def tick_now() -> dict:
"""Sofortiger Trigger-Check — nicht warten auf den naechsten Loop-Tick.
Wird genutzt wenn ein neues GPS-Update reinkommt: Bridge ruft das nach
_persist_location, damit Watcher mit near() den frischen Wert sofort
sehen statt bis zu TICK_SEC Sekunden zu warten."""
if _AGENT_FACTORY is None:
return {"ok": False, "error": "Background-Loop noch nicht gestartet"}
try:
await _tick(_AGENT_FACTORY)
return {"ok": True}
except Exception as exc:
logger.exception("tick_now: %s", exc)
return {"ok": False, "error": str(exc)}
async def run_loop(agent_factory) -> None: async def run_loop(agent_factory) -> None:
"""Endlosschleife — wird vom main lifespan gestartet + gestoppt.""" """Endlosschleife — wird vom main lifespan gestartet + gestoppt."""
global _AGENT_FACTORY
_AGENT_FACTORY = agent_factory
logger.info("Trigger-Loop gestartet (TICK_SEC=%d)", TICK_SEC) logger.info("Trigger-Loop gestartet (TICK_SEC=%d)", TICK_SEC)
while True: while True:
try: try:
+10
View File
@@ -657,6 +657,16 @@ def triggers_list(active_only: bool = False):
return {"triggers": triggers_mod.list_triggers(active_only=active_only)} return {"triggers": triggers_mod.list_triggers(active_only=active_only)}
@app.post("/triggers/check-now")
async def triggers_check_now():
"""Sofortiger Trigger-Check, statt auf den naechsten Background-Tick
zu warten. Wird von der Bridge nach jedem location_update gerufen
damit GPS-Watcher (near()) den frischen Wert SOFORT sehen — bei
Auto-Vorbeifahrt durch einen 300m-Radius hat man sonst nur ~20s
Drinnen-Zeit, was unter TICK_SEC fallen kann."""
return await background_mod.tick_now()
@app.get("/triggers/conditions") @app.get("/triggers/conditions")
def triggers_conditions(): def triggers_conditions():
"""Verfuegbare Variablen + Funktionen fuer Watcher-Conditions """Verfuegbare Variablen + Funktionen fuer Watcher-Conditions
+81 -7
View File
@@ -25,7 +25,7 @@ import shutil
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Dict, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -91,6 +91,12 @@ def _cpu_load_1min() -> float:
_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] _DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
# Maximales GPS-Alter fuer near()-Auswertung. Wenn die App laenger nicht
# gepushed hat (z.B. Tracking aus, Mobilfunk weg, App geschlossen), gilt
# die Position als "unbekannt" und near() liefert False — verhindert
# Phantom-Fires basierend auf einer wochen-alten Position.
NEAR_MAX_AGE_SEC = 5 * 60
def _gps_state() -> dict[str, Any]: def _gps_state() -> dict[str, Any]:
"""Letzte bekannte Position aus /shared/state/location.json. """Letzte bekannte Position aus /shared/state/location.json.
@@ -119,8 +125,22 @@ def _user_activity_age() -> int:
return int(time.time() - ts) return int(time.time() - ts)
def collect_variables() -> dict[str, Any]: def _near_key(lat: float, lon: float, radius_m: float) -> str:
"""Liefert aktuellen Snapshot aller Built-in-Variablen + near()-Helper.""" """Stabiler Schluessel pro near()-Aufruf — fuer entered_near/left_near
State-Tracking pro Trigger pro Aufrufstelle."""
return f"{float(lat):.6f},{float(lon):.6f},{int(float(radius_m))}"
def collect_variables(prev_near_states: Optional[Dict[str, bool]] = None) -> Dict[str, Any]:
"""Liefert aktuellen Snapshot aller Built-in-Variablen + near()-Helper.
prev_near_states: pro Trigger gespeicherter Zustand vom letzten Eval
(für entered_near/left_near). Wird vom background-Loop reingegeben.
Nach dem Eval kann man `vars_['_new_near_states']` auslesen, um den
Update-Snapshot zurueck ins Trigger-Manifest zu schreiben."""
if prev_near_states is None:
prev_near_states = {}
new_near_states: Dict[str, bool] = {}
free_gb, free_pct = _disk_stats() free_gb, free_pct = _disk_stats()
now = datetime.now() now = datetime.now()
gps = _gps_state() gps = _gps_state()
@@ -176,12 +196,17 @@ def collect_variables() -> dict[str, Any]:
# Funktion-Helper — wird vom Parser als ast.Call mit Name "near" erkannt. # Funktion-Helper — wird vom Parser als ast.Call mit Name "near" erkannt.
# Closure ueber die GPS-Werte, damit eval keine extra Variablen braucht. # Closure ueber die GPS-Werte, damit eval keine extra Variablen braucht.
def _near(lat: float, lon: float, radius_m: float) -> bool: def _compute_near(lat: float, lon: float, radius_m: float) -> bool:
"""Haversine-Distanz: True wenn aktuelle Position < radius_m vom Punkt.""" """Haversine-Distanz: True wenn aktuelle Position < radius_m vom Punkt.
Plus Age-Schutz: GPS-Daten aelter als NEAR_MAX_AGE_SEC werden als
veraltet betrachtet → False."""
cur_lat = vars_.get("current_lat") cur_lat = vars_.get("current_lat")
cur_lon = vars_.get("current_lon") cur_lon = vars_.get("current_lon")
if cur_lat is None or cur_lon is None: if cur_lat is None or cur_lon is None:
return False return False
age = vars_.get("location_age_sec")
if isinstance(age, (int, float)) and age >= 0 and age > NEAR_MAX_AGE_SEC:
return False
try: try:
R = 6371000.0 R = 6371000.0
phi1 = math.radians(float(cur_lat)) phi1 = math.radians(float(cur_lat))
@@ -194,7 +219,39 @@ def collect_variables() -> dict[str, Any]:
except Exception: except Exception:
return False return False
def _near(lat: float, lon: float, radius_m: float) -> bool:
"""True solange im Radius drin. Plus State-Tracking fuer
entered_near/left_near — wir merken uns das letzte Ergebnis
damit Uebergaenge erkannt werden koennen."""
current = _compute_near(lat, lon, radius_m)
new_near_states[_near_key(lat, lon, radius_m)] = current
return current
def _entered_near(lat: float, lon: float, radius_m: float) -> bool:
"""True NUR beim Uebergang draussen → innen. Use-Case: einmal
feuern wenn der User in den Radius reinfaehrt (Blitzer-Warner,
Ankunft-Erinnerung). Bei groesserem Radius = Vorwarnung."""
current = _compute_near(lat, lon, radius_m)
key = _near_key(lat, lon, radius_m)
new_near_states[key] = current
prev = bool(prev_near_states.get(key, False))
return current and not prev
def _left_near(lat: float, lon: float, radius_m: float) -> bool:
"""True NUR beim Uebergang innen → draussen. Use-Case: 'Hast
du am Parkplatz X was vergessen?' beim Verlassen."""
current = _compute_near(lat, lon, radius_m)
key = _near_key(lat, lon, radius_m)
new_near_states[key] = current
prev = bool(prev_near_states.get(key, False))
return prev and not current
vars_["near"] = _near vars_["near"] = _near
vars_["entered_near"] = _entered_near
vars_["left_near"] = _left_near
# Update-Snapshot fuer den Caller (background-Loop schreibt das pro
# Trigger zurueck damit beim naechsten Tick prev_near_states stimmt)
vars_["_new_near_states"] = new_near_states
return vars_ return vars_
@@ -236,8 +293,25 @@ def describe_functions() -> list[dict]:
{ {
"name": "near", "name": "near",
"signature": "near(lat, lon, radius_m)", "signature": "near(lat, lon, radius_m)",
"desc": "True wenn die aktuelle GPS-Position innerhalb von radius_m Metern " "desc": "True SOLANGE die aktuelle GPS-Position innerhalb von radius_m "
"vom Punkt (lat, lon) liegt. Haversine. Bei unbekannter Position: False.", "Metern vom Punkt (lat, lon) liegt. Feuert wiederholt (mit throttle). "
"Use-Case: 'bin noch in der Naehe von X?'. "
"Haversine. Bei unbekannter oder > 5min alter Position: False.",
},
{
"name": "entered_near",
"signature": "entered_near(lat, lon, radius_m)",
"desc": "True NUR im Moment des Eintritts in den Radius (Uebergang "
"draussen → innen). Use-Case: einmaliger Fire bei Ankunft / "
"Blitzer-Warnung. Mit grossem Radius (z.B. 2000) wird das zur "
"Vorwarnung bevor man am Punkt ist.",
},
{
"name": "left_near",
"signature": "left_near(lat, lon, radius_m)",
"desc": "True NUR im Moment des Verlassens des Radius (Uebergang "
"innen → draussen). Use-Case: 'Hast du am Parkplatz X was "
"vergessen?' beim Wegfahren.",
}, },
] ]
+51 -1
View File
@@ -938,7 +938,12 @@ class ARIABridge:
def _persist_location(self, location: Optional[dict]) -> None: def _persist_location(self, location: Optional[dict]) -> None:
"""Speichert die letzte bekannte GPS-Position fuer Watcher. """Speichert die letzte bekannte GPS-Position fuer Watcher.
Erwartet {lat, lon} oder {lat, lng}. Nicht-Dicts und fehlende Erwartet {lat, lon} oder {lat, lng}. Nicht-Dicts und fehlende
Koordinaten werden ignoriert.""" Koordinaten werden ignoriert.
Plus: triggert sofort einen on-demand Trigger-Check im Brain
(POST /triggers/check-now). Ohne das wartet der Watcher-Loop
bis zu TICK_SEC Sekunden — bei Auto-Vorbeifahrt durch einen
300m-Radius (18-43s drin) kann das den Trigger verpassen."""
if not isinstance(location, dict): if not isinstance(location, dict):
return return
try: try:
@@ -950,9 +955,31 @@ class ARIABridge:
"lat": float(lat), "lat": float(lat),
"lon": float(lon), "lon": float(lon),
}) })
except Exception:
return
# Fire-and-forget: Brain-on-demand-Tick. Wenn Brain nicht antwortet
# oder langsam ist, blockt das nicht den GPS-Pfad.
try:
asyncio.create_task(self._trigger_brain_check_now())
except Exception: except Exception:
pass pass
async def _trigger_brain_check_now(self) -> None:
"""Brain-Endpoint POST /triggers/check-now anstossen."""
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
def _post():
try:
req = urllib.request.Request(
f"{brain_url}/triggers/check-now",
data=b"", method="POST",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=8) as r:
return r.status
except Exception:
return None
await asyncio.get_event_loop().run_in_executor(None, _post)
def _persist_user_activity(self) -> None: def _persist_user_activity(self) -> None:
"""Markiert dass der User gerade etwas gemacht hat (Chat/Voice). """Markiert dass der User gerade etwas gemacht hat (Chat/Voice).
Watcher: last_user_message_ago_sec basiert darauf.""" Watcher: last_user_message_ago_sec basiert darauf."""
@@ -1824,6 +1851,29 @@ class ARIABridge:
logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error")) logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error"))
return return
elif msg_type == "app_log":
# App schickt Crash/Error/Info-Log via RVS — wir schreiben das
# in /shared/logs/app.log (JSONL) damit Diagnostic + Claude
# mitlesen koennen, auch ohne ADB-Zugriff aufs Handy.
try:
log_dir = Path("/shared/logs")
log_dir.mkdir(parents=True, exist_ok=True)
line = {
"ts": payload.get("ts") or int(time.time() * 1000),
"platform": payload.get("platform", "?"),
"level": payload.get("level", "info"),
"scope": payload.get("scope", ""),
"message": payload.get("message", ""),
"stack": payload.get("stack", ""),
}
with (log_dir / "app.log").open("a", encoding="utf-8") as f:
f.write(json.dumps(line, ensure_ascii=False) + "\n")
logger.info("[app-log] %s %s: %s",
line["level"], line["scope"], line["message"][:120])
except Exception as exc:
logger.warning("[app-log] schreiben fehlgeschlagen: %s", exc)
return
elif msg_type == "brain_request": elif msg_type == "brain_request":
# Generischer RVS-Proxy fuer die Brain-HTTP-API. # Generischer RVS-Proxy fuer die Brain-HTTP-API.
# payload: {requestId, method, path, body?, bodyBase64?, contentType?} # payload: {requestId, method, path, body?, bodyBase64?, contentType?}
+36
View File
@@ -1338,6 +1338,42 @@ const server = http.createServer((req, res) => {
else broadcast({ type: "agent_activity", activity: "idle" }); else broadcast({ type: "agent_activity", activity: "idle" });
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));
} else if (req.url.startsWith("/api/app-log") && req.method === "GET") {
// App-Crash-Reporting-Log lesen — die App schickt JS-Errors via RVS,
// Bridge schreibt JSONL nach /shared/logs/app.log. Wir liefern die
// letzten 200 Eintraege (oder ?limit=N).
const url = new URL(req.url, "http://x");
const limit = Math.max(1, Math.min(2000, parseInt(url.searchParams.get("limit") || "200", 10) || 200));
try {
const file = "/shared/logs/app.log";
if (!fs.existsSync(file)) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ count: 0, entries: [] }));
return;
}
const raw = fs.readFileSync(file, "utf-8");
const lines = raw.split("\n").filter(l => l.trim());
const tail = lines.slice(-limit);
const entries = tail.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ count: entries.length, entries }));
} catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
return;
} else if (req.url === "/api/app-log/clear" && req.method === "POST") {
// App-Log leeren — nach erfolgreichem Debug.
try {
const file = "/shared/logs/app.log";
if (fs.existsSync(file)) fs.unlinkSync(file);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message }));
}
return;
} else if (req.url === "/api/files-list" && req.method === "GET") { } else if (req.url === "/api/files-list" && req.method === "GET") {
// Liste alle Dateien in /shared/uploads/ — die kommen entweder vom User // Liste alle Dateien in /shared/uploads/ — die kommen entweder vom User
// (Upload aus App/Diagnostic) oder von ARIA (aria_<name>.<ext> Pattern). // (Upload aus App/Diagnostic) oder von ARIA (aria_<name>.<ext> Pattern).
+17 -1
View File
@@ -297,6 +297,23 @@ Skills mit Tool-Use.
- [x] **Gehirn-Kategorien standardmaessig eingeklappt**: Beim ersten Aufruf alle Type-Sections collapsed, Stefan klappt gezielt auf was er sehen will. State persistiert in localStorage - [x] **Gehirn-Kategorien standardmaessig eingeklappt**: Beim ersten Aufruf alle Type-Sections collapsed, Stefan klappt gezielt auf was er sehen will. State persistiert in localStorage
- [x] **Klappbare Type-Header + Category-AutoSuggest + Info-Modal**: Type-Header (▼/▶) klappbar, Category-Feld im Neu/Edit-Modal mit `<datalist>`-Vorschlaegen aller existierenden Categories, -Button-Modal erklaert welche Types FEST im System-Prompt vs. Cold Memory sind - [x] **Klappbare Type-Header + Category-AutoSuggest + Info-Modal**: Type-Header (▼/▶) klappbar, Category-Feld im Neu/Edit-Modal mit `<datalist>`-Vorschlaegen aller existierenden Categories, -Button-Modal erklaert welche Types FEST im System-Prompt vs. Cold Memory sind
### GPS-Trigger-Verbesserungen (entered_near + left_near + Timing-Fix)
- [x] **near() bei Auto-Vorbeifahrten verpasst — gefixt**: Background-Loop tickte alle 30s, Vorbeifahrt durch 300m-Radius bei 50-120 km/h dauert nur 18-43s → Tick konnte komplett dazwischen liegen. Fix: `TICK_SEC` 30 → 8 (Loop ist billig, Brain merkt das nicht). Plus event-getrieben: Bridge ruft nach jedem `location_update` ein POST `/triggers/check-now` im Brain → Watcher sehen die frische Position in Millisekunden statt im Polling-Takt. Polling läuft parallel als Fallback für Watcher ohne GPS-Bezug
- [x] **near() Age-Schutz**: GPS-Daten älter als 5 Minuten (`NEAR_MAX_AGE_SEC=300`) gelten als veraltet → `near()` liefert False. Vorher hätte ein wochen-alter Wert die Funktion weiter als „in der Nähe" eingeordnet → Phantom-Fires wenn Tracking aus war
- [x] **Drei GPS-Modi statt einem**: `near()` bleibt = „solange drin". Neu: **`entered_near(lat, lon, r)`** feuert NUR beim Übergang außen→innen (Blitzer-Warner mit r=2000 = 2 km Vorwarnung, Ankunft mit r=100), **`left_near(lat, lon, r)`** feuert NUR beim Übergang innen→außen („Hast du am Parkplatz was vergessen?"). State-Tracking pro Trigger pro near-Aufruf (`near_states`-Dict im Manifest) — Background-Loop schreibt den letzten Auswertungswert immer zurück, damit beim nächsten Tick die Übergangs-Erkennung greift. ARIA's `trigger_watcher`-Tool-Description erklärt die drei Modi inkl. empfohlener Throttle-Werte (kurz für entered/left, lang für near)
### App-Memory-Editor + Crash-Reporting
- [x] **Bubble-Header dynamic** (created/updated/deleted): Die `🧠`-Bubble zeigt jetzt was passiert ist — "ARIA hat etwas gemerkt" / "Notiz geändert" / "Notiz gelöscht" (rot bei delete). Brain-Tools schicken `action`-Feld im memory_saved-Event mit
- [x] **Tap auf Memory-Bubble → Detail-Modal**: Komponente `MemoryDetailModal` zeigt alle Felder (Titel, Type, Category, Tags, voller Content, Anhang-Vorschau mit Thumbnails). Stift-Icon wechselt in Edit-Mode mit Form-Feldern + 📌 Pinned-Toggle. **Anhänge hoch-/runterladen + löschen** im Modal (DocumentPicker, multipart-Upload via RVS-Brain-Proxy). Memory komplett löschen mit Confirm
- [x] **Notizen-Inbox-Button (`🗂️`)** neben der Lupe in der Status-Leiste: Vollbild-Modal mit zwei Sections — „Aus diesem Chat" (kompakte Liste der Spezial-Bubbles aus dem aktuellen Verlauf, klickbar) + „Alle Memories aus der DB" mit dem `MemoryBrowser`. Spezial-Bubbles (memorySaved/triggerCreated/skillCreated) werden im Chat-Stream gefiltert (statt unten zu kleben)
- [x] **Memory-Editor in App-Settings**: neue Sektion 🧠 „Gedächtnis" in den App-Einstellungen. Komplette CRUD-UI mit Wortlich-Suche, Type-Dropdown, Pinned/Cold-Filter, „+ Neu" anlegen. Selbe `MemoryBrowser`-Komponente wie in der Inbox
- [x] **RVS-Brain-Proxy als Fundament**: Bridge implementiert generischen `brain_request` / `brain_response`-Channel — die App kann beliebige Brain-HTTP-Endpoints via RVS adressieren (GET/POST/PATCH/DELETE, JSON+Base64-Body, base64-encoded Binär-Antworten). `services/brainApi.ts` als Promise-basierter Client mit Request-ID-Routing, Timeout, automatischem Listener-Setup
- [x] **App-Crash-Reporting via RVS**: ErrorBoundary-Komponente fängt React-Render-Fehler, `installGlobalCrashReporter` haengt sich an `ErrorUtils.setGlobalHandler` + `HermesInternal.enablePromiseRejectionTracker`. Crashes wandern als `app_log`-Event durch RVS, Bridge schreibt JSONL in `/shared/logs/app.log`. Diagnostic-Server liefert GET `/api/app-log[?limit=N]` + POST `/api/app-log/clear`. **`tools/fetch-app-logs.sh`** holt die Logs auf die Dev-Maschine (über `ARIA_DIAG_URL` aus `.claude/aria-vm.env`), speichert in `.aria-debug/` (gitignored), zeigt Stack-Trace kompakt auf stdout
- [x] **`memory_search` + `memory_update` Tools**: ARIA kann die DB jetzt aktiv durchsuchen (Volltext/Semantic) und existierende Einträge per ID patchen statt fragmentierende neue anzulegen. Tool-Description sagt explizit „Memory ist Truth über Conversation-Window" — wenn der User korrigiert hat, gilt das was im Memory steht. Wichtig nach Diagnostic-Edits damit ARIA die neue Wahrheit sieht statt aus dem Window zu raten
- [x] **App-Bugfixes**: (a) URLSearchParams crasht in Hermes — durch Mini-Query-Builder ersetzt (`brainApi._qs()`). (b) Cache leer + Datei-Tap → Auto-Re-Download via file_request statt Toast-Sackgasse, plus State-Cleanup (uri/localUri auf undefined). (c) Memory-Liste in Settings scrollt jetzt (nestedScrollEnabled auf FlatList + äußere ScrollView). (d) Modal-im-Modal auf Android gefixt — MemoryBrowser nimmt optionalen `onOpenMemory`-Callback, kein verschachteltes DetailModal mehr. (e) Alert.prompt (iOS-only) durch eigenes Text-Input-Modal ersetzt fuer „Neue Memory anlegen"
### Memory-Anhaenge mit Vision (Stufe A-E + attach_paths) ### Memory-Anhaenge mit Vision (Stufe A-E + attach_paths)
- [x] **Anhaenge an Memory-Eintraege** — Bilder/PDFs/beliebige Dateien koennen an jede Memory gehaengt werden, liegen physisch unter `/shared/memory-attachments/<memory-id>/`. Cleanup beim Memory-Delete automatisch. Limit 20 MB pro Datei - [x] **Anhaenge an Memory-Eintraege** — Bilder/PDFs/beliebige Dateien koennen an jede Memory gehaengt werden, liegen physisch unter `/shared/memory-attachments/<memory-id>/`. Cleanup beim Memory-Delete automatisch. Limit 20 MB pro Datei
@@ -331,7 +348,6 @@ Skills mit Tool-Use.
- [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild) - [ ] Custom-Wake-Word-Upload via Diagnostic (eigene .onnx-Files ohne App-Rebuild)
### Architektur ### Architektur
- [ ] Bilder: Claude Vision direkt nutzen (aktuell nur Dateipfad an ARIA)
- [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU) - [ ] Diagnostic: System-Info Tab (Container-Status, Disk, RAM, CPU)
- [ ] RVS Zombie-Connections endgueltig loesen - [ ] RVS Zombie-Connections endgueltig loesen
- [ ] Gamebox: kleine Web-Oberflaeche fuer Credentials/Server-Config oder zentral aus Diagnostic per RVS push - [ ] Gamebox: kleine Web-Oberflaeche fuer Credentials/Server-Config oder zentral aus Diagnostic per RVS push
+1
View File
@@ -31,6 +31,7 @@ const ALLOWED_TYPES = new Set([
"chat_history_request", "chat_history_response", "chat_cleared", "chat_history_request", "chat_history_response", "chat_cleared",
"delete_message_request", "chat_message_deleted", "delete_message_request", "chat_message_deleted",
"brain_request", "brain_response", "brain_request", "brain_response",
"app_log",
"file_delete_batch_request", "file_delete_batch_response", "file_delete_batch_request", "file_delete_batch_response",
"file_zip_request", "file_zip_response", "file_zip_request", "file_zip_response",
"xtts_delete_voice", "xtts_delete_voice",
+34
View File
@@ -0,0 +1,34 @@
# tools/
Hilfsskripte für die Dev-Maschine. Brauchen `.claude/aria-vm.env` (aus
`.example` kopieren + lokale VM-IP eintragen).
## fetch-app-logs.sh
Holt App-Crash-Logs von der VM und speichert sie unter `.aria-debug/`
(gitignored). Die App schickt JS-Errors und ungefangene Promise-
Rejections via RVS an die Bridge — Bridge sammelt in
`/shared/logs/app.log`, Diagnostic-Server gibt sie via
`GET /api/app-log` raus.
```bash
tools/fetch-app-logs.sh # 200 neueste Eintraege
tools/fetch-app-logs.sh --limit 50 # weniger
tools/fetch-app-logs.sh --watch # alle 5s pollen, neue rausgeben
tools/fetch-app-logs.sh --clear # nach Abholen Log auf VM leeren
```
Ausgabe enthaelt pro Eintrag: Uhrzeit, Level (error/warn/info), Scope
(z.B. `ChatScreen.InboxModal` oder `global-fatal`), Message, und die
ersten ~8 Stack-Frames. Die kompletten Daten liegen als JSON in
`.aria-debug/app-log-<timestamp>.json`.
Workflow nach einem Crash:
1. App rebuilden mit Crash-Reporting (passiert automatisch ab dem
`21a315c`-Commit)
2. Crash in der App ausloesen
3. `tools/fetch-app-logs.sh` auf der Dev-Maschine
4. Stacktrace lesen / Claude geben
5. Fix bauen
6. `tools/fetch-app-logs.sh --clear` damit der Log wieder sauber ist
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# fetch-app-logs.sh — App-Crash-Logs von der VM holen
#
# Nutzt .claude/aria-vm.env als Quelle fuer $ARIA_DIAG_URL und ruft
# GET /api/app-log?limit=N. Speichert die Roh-Response unter
# .aria-debug/app-log-<timestamp>.json und gibt eine kompakte
# Zusammenfassung auf stdout aus (letzte Eintraege mit Stack-Trace).
#
# Verwendung:
# tools/fetch-app-logs.sh # Default limit=200
# tools/fetch-app-logs.sh --limit 50 # nur 50 holen
# tools/fetch-app-logs.sh --clear # nach Abholen Log loeschen
# tools/fetch-app-logs.sh --watch # alle 5s pollen, neue Eintraege ausgeben
set -euo pipefail
LIMIT=200
CLEAR=0
WATCH=0
while [[ $# -gt 0 ]]; do
case "$1" in
--limit) LIMIT="$2"; shift 2 ;;
--limit=*) LIMIT="${1#*=}"; shift ;;
--clear) CLEAR=1; shift ;;
--watch) WATCH=1; shift ;;
-h|--help)
sed -n '1,/^set/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'
exit 0 ;;
*) echo "Unbekannte Option: $1" >&2; exit 1 ;;
esac
done
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ENV_FILE="$ROOT/.claude/aria-vm.env"
OUT_DIR="$ROOT/.aria-debug"
if [[ ! -f "$ENV_FILE" ]]; then
echo "FEHLER: $ENV_FILE nicht vorhanden. Aus .example kopieren und IP anpassen." >&2
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
if [[ -z "${ARIA_DIAG_URL:-}" ]]; then
echo "FEHLER: ARIA_DIAG_URL nicht gesetzt in $ENV_FILE" >&2
exit 1
fi
mkdir -p "$OUT_DIR"
fetch_once() {
local ts json file
ts="$(date +%Y%m%d_%H%M%S)"
json="$(curl -s --max-time 10 "${ARIA_DIAG_URL%/}/api/app-log?limit=$LIMIT")" || {
echo "FEHLER: curl gegen $ARIA_DIAG_URL fehlgeschlagen" >&2
return 1
}
file="$OUT_DIR/app-log-$ts.json"
echo "$json" > "$file"
python3 - "$file" <<'PY'
import json, sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text())
entries = data.get("entries") or []
print(f"=== {len(entries)} Eintrag{'e' if len(entries)!=1 else ''} (gespeichert unter {sys.argv[1]}) ===")
for e in entries[-20:]:
ts = e.get("ts") or 0
from datetime import datetime
when = datetime.fromtimestamp(ts/1000).strftime("%H:%M:%S") if ts else "?"
lvl = e.get("level","?")
scope = e.get("scope","?")
msg = (e.get("message") or "").splitlines()[0][:200]
print(f"\n[{when}] {lvl:5} {scope}: {msg}")
stack = (e.get("stack") or "").strip()
if stack:
for line in stack.splitlines()[:8]:
print(f" {line}")
if len(stack.splitlines()) > 8:
print(f" ... ({len(stack.splitlines())-8} weitere Zeilen — siehe JSON)")
PY
return 0
}
if [[ "$WATCH" == "1" ]]; then
echo "Watching $ARIA_DIAG_URL/api/app-log — Ctrl+C zum Beenden"
SEEN=""
while true; do
cur=$(curl -s --max-time 10 "${ARIA_DIAG_URL%/}/api/app-log?limit=$LIMIT") || cur=""
hash=$(echo "$cur" | md5sum | awk '{print $1}')
if [[ "$hash" != "$SEEN" && -n "$cur" ]]; then
SEEN="$hash"
fetch_once
fi
sleep 5
done
else
fetch_once
fi
if [[ "$CLEAR" == "1" ]]; then
echo
echo "→ Log auf der VM leeren..."
curl -s --max-time 5 -X POST "${ARIA_DIAG_URL%/}/api/app-log/clear" | python3 -m json.tool || true
fi