Compare commits

..
405 Commits
Author SHA1 Message Date
duffyduck ebdb306c03 release: bump version to 0.1.2.8 2026-05-12 16:45:29 +02:00
duffyduckandClaude Opus 4.7 f9b55012f2 feat(chat): Muelltonne pro Bubble — gezielt eine Nachricht loeschen
Stefan kann jetzt einzelne Chat-Bubbles loeschen (mit Rueckfrage).
Die Bubble verschwindet aus chat_backup.jsonl (Bridge), Brain-
Conversation (rolling window + jsonl) und allen Clients (App +
Diagnostic). Genauso wichtig fuer ARIA: der gloeschte Turn ist im
naechsten Chat-Prompt nicht mehr im Window.

Pipeline:
  UI 🗑 + confirm
  → RVS delete_message_request {ts}
  → Bridge._delete_chat_message:
      - chat_backup.jsonl Zeile mit ts entfernen (atomar via tmp+rename)
      - Brain POST /conversation/delete-turn (role+content match)
      - RVS broadcast chat_message_deleted {ts}
  → App + Diagnostic entfernen Bubble lokal per ts-Match

Backend-Aenderungen:
- aria-brain/conversation.py: remove_by_match(role, content, ts_hint)
  + _rewrite_file (atomar). Match nahester Turn bei mehrfach gleichem
  content.
- aria-brain/main.py: POST /conversation/delete-turn (POST statt DELETE
  weil FastAPI keine Bodys auf DELETE erlaubt)
- bridge/aria_bridge.py: HTTP-Listener /internal/delete-chat-message
  + RVS-Handler delete_message_request. _append_chat_backup gibt jetzt
  ts zurueck, _process_core_response packt backupTs ins chat-Event.
- rvs/server.js: ALLOWED_TYPES um delete_message_request +
  chat_message_deleted erweitert.
- diagnostic/server.js: delete_chat_message-Action + chat_message_deleted
  Relay zum Browser.

Frontend-Aenderungen:
- diagnostic/index.html: 🗑 erscheint on-hover in Bubbles mit data-ts,
  confirm()-Dialog, addChat + chat_history setzen data-ts. WS-Listener
  fuer chat_message_deleted entfernt Bubble per data-ts.
- android/ChatScreen.tsx: backupTs in ChatMessage, Muelltonne-Button
  unten rechts in jeder Bubble, Alert-confirm, RVS-Listener fuer
  chat_message_deleted entfernt aus messages-State.

Live-User-Bubbles (sofort gerendert vom eigenen Send) haben noch
keinen backupTs bis der Bridge-Roundtrip durch ist — die Muelltonne
erscheint dort erst nach kurzer Verzoegerung / Reload. Folgekommit
kann das polieren wenn noetig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:42:20 +02:00
duffyduckandClaude Opus 4.7 fee0f061b8 fix(brain): Memory-Suche filtert jetzt Rauschen — score_threshold + kleineres k
Bug: bei kleiner DB (31 Eintraege) lieferte die Suche fuer JEDES Wort
fast alles als Treffer zurueck — k=20 Top-N ohne Threshold sorgte
dafuer dass auch "banane" zehn vermeintliche Treffer mit Scores
0.09-0.22 (= Rauschen) zurueckgab.

Fix:
- vector_store.search() bekommt optional score_threshold (an Qdrant
  durchgereicht, das nimmt's nativ)
- /memory/search endpoint hat score_threshold-Query-Param (default 0.30)
- Diagnostic schickt k=10 + score_threshold=0.30 statt k=20 ohne Threshold
- "Keine Treffer"-Info-Box wenn alle Treffer < Threshold

MiniLM-multilingual liefert typischerweise:
  >0.50 → starker Treffer
  0.30-0.50 → relevant
  0.20-0.30 → grenzwertig
  <0.20 → Rauschen

Mit score_threshold=0 (oder None) bleibt die alte Top-N-Semantik
fuer Aufrufer die Rauschen explizit wollen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:59:05 +02:00
duffyduckandClaude Opus 4.7 7ead1212e6 chore: brain-import/ wird komplett ignoriert (Drop-Folder)
Stefan wirft MDs rein wenn er was migrieren will, klickt im
Diagnostic-Gehirn-Tab auf "Migration aus brain-import/", fertig.
Was nicht migriert ist, liegt halt rum — gehoert aber nicht ins Repo
(private Daten, ephemerer Kram).

.gitignore-Pattern:
  aria-data/brain-import/*
  !.gitkeep
  !README.md

Alte spezifische USER.md-Zeile durch das catch-all ersetzt — wir
mussten USER.md.example und Co. eh nicht mehr im Repo halten.

README in dem Verzeichnis entsprechend angepasst (Drop-Folder, nicht
"leerer Restposten").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:49:03 +02:00
duffyduckandClaude Opus 4.7 282db900d4 chore: brain-import/-MDs raus — DB ist Truth, README + .gitkeep statt Saatgut
AGENT.md/BOOTSTRAP.md enthielten Duplikate, OpenClaw-Referenzen und
fast-Memory-Hinweise auf das alte file-basierte System. Nach dem
DB-Cleanup (60 → 31 Eintraege) sind die alten MDs nicht mehr nuetzlich
— Stefan kuratiert direkt im Diagnostic-Gehirn-Tab, Backup laeuft via
Bootstrap-Snapshot (JSON) oder Komplett-tar.gz.

TOOLING.md.example + USER.md.example mit raus (auch obsolet).
.gitkeep haelt das Verzeichnis im Repo, README dokumentiert wofuer
es mal war und wann man es wieder braucht (Disaster-Recovery ohne
Snapshot, neues ARIA von Null).

Migration-Code (aria-brain/migration.py) bleibt — falls jemand mal
frische MDs reinpackt um sie zu parsen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:45:50 +02:00
duffyduckandClaude Opus 4.7 f4008f44cb chore(claude): .claude/*.env gitignored — .example als Vorlage commited
Damit kann die Dev-Maschine (wo Claude Code laeuft) die aria-wohnung-VM
ueber Diagnostic Port 3001 erreichen, ohne die interne IP im Git zu
haben. Pro Maschine wird .claude/aria-vm.env aus dem .example kopiert
und mit der lokalen Routing-Info gefuellt.

Nutzung:
  source .claude/aria-vm.env
  curl -s "$ARIA_BRAIN_URL/memory/stats"

Im docker-compose-Netz aria-net leben die Hostnamen (aria-brain etc.)
weiterhin direkt — das brauchst nur Hosts AUSSERHALB der VM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:31:32 +02:00
duffyduckandClaude Opus 4.7 baac815df1 ux(diag): Gehirn-Kategorien standardmaessig eingeklappt
Beim ersten Aufruf (kein localStorage-Eintrag) sind alle Type-Sections
collapsed. Stefan klappt gezielt auf was er sehen will, statt eine
Wand of Text zu sehen. Sobald er einmal getoggelt hat, ueberschreibt
sein persistiertes State den Default — also nicht aufdringlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:15:22 +02:00
duffyduckandClaude Opus 4.7 fdb7edfcbd feat(diag): Memory-Druckansicht — Strg+P → als PDF
Neuer Button "📄 Drucken / PDF" im Gehirn-Tab oeffnet eine sauber
formatierte Print-View in neuem Tab. Druck-CSS optimiert (page-break-
inside:avoid pro Entry, schwarze Borders fuer Print, Action-Bar wird
versteckt). Aktueller Type+Pinned-Filter wird respektiert.

Browser-eigenes "Als PDF speichern" greift dann — kein Tool noetig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:06:45 +02:00
duffyduckandClaude Opus 4.7 b0f3a7b667 feat(diag): Gehirn-Tab — klappbare Type-Header + Category-AutoSuggest + Info-Modal
UX im Memory-Browser geschaerft, Stefan-Wunsch:

1. Klappbare Type-Gruppen:
   Jeder Type-Header (Identität, Regeln, ...) hat jetzt einen ▼/▶
   Indikator und reagiert auf Click. Eingeklappte Sektionen werden
   in localStorage gemerkt — bleiben ueber Reloads stabil.

2. Category-AutoSuggest:
   Das Kategorie-Feld im Neu/Edit-Modal hat jetzt ein <datalist>
   mit allen schon in der DB existierenden Categories als Vorschlag.
   Neue Categories sind weiterhin frei eintippbar. Liste wird bei
   jedem renderBrainList-Aufruf aus dem Cache aktualisiert.

3. Info-Button (ℹ) neben dem Typ-Dropdown:
   Erklaert welche Types FEST im System-Prompt eine eigene Sektion
   bekommen (identity/rule/preference/tool/skill — Hot Memory)
   und welche nur via semantischer Cold-Search reinkommen (fact/
   conversation/reminder). Konsistent mit prompts.py:TYPE_HEADINGS.
   Auch dokumentiert dass Category ein freier Tag ist und den
   Prompt nicht direkt beeinflusst.

Type-Dropdown-Labels selbst zeigen jetzt (FEST) / (Cold) als Hinweis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:01:28 +02:00
duffyduckandClaude Opus 4.7 92da39ba17 docs: README + issue — Proxy-Tool-Use-Patch + Trigger-Reply-Push dokumentiert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:52:04 +02:00
duffyduckandClaude Opus 4.7 0b222fbe46 fix(trigger): Trigger-Antworten landen jetzt im Chat — Brain → Bridge Push
Bug: Wenn der Brain-Background-Loop einen Timer/Watcher feuert, ruft
er agent.chat() direkt im eigenen Prozess. Die Antwort wurde nur ins
Trigger-Log geschrieben — kein RVS-Broadcast, kein TTS, nichts in
App/Diagnostic sichtbar.

Fix: Bridge ↔ Brain bekommen einen internen HTTP-Push-Kanal.

Bridge (Port 8090, nicht exposed, nur aria-net intern):
  asyncio.start_server-basierter HTTP-Listener.
  POST /internal/trigger-fired
    body: {reply, trigger_name, type, events}
  → _handle_trigger_fired feuert Side-Channel-Events
    (trigger_created/skill_created/location_tracking) erst,
    dann _process_core_response(reply) — exakt der gleiche Pfad
    wie normale Chat-Antworten (Chat-Bubble + TTS + chat_backup).

Brain background.py:
  Nach agent.chat() in _fire wird agent.pop_events() ausgelesen
  und zusammen mit dem Reply via urllib an aria-bridge:8090
  gepostet (run_in_executor damit es den asyncio-Loop nicht
  blockiert). Failures werden geloggt, der Trigger selbst bleibt
  trotzdem als 'fired' markiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:50:32 +02:00
duffyduckandClaude Opus 4.7 a9f0f7be83 feat(proxy): Tool-Use durchreichen — eigene Adapter-Files ueberschreiben npm-Version
Der claude-max-api-proxy ignoriert das OpenAI-tools-Feld komplett:
openai-to-cli.js wandelt nur messages in einen String, manager.js
spawnt 'claude --print' ohne Tools. Claude Code nutzt dann ihre
internen Tools (Bash, etc.) — bei 'Timer in 2min' macht sie ein
'sleep 120' intern und meldet 'erledigt' ohne dass wir je einen
trigger_timer-Call sehen.

Fix: zwei eigene Adapter-Files unter proxy-patches/ die zur
Container-Startzeit ueber die npm-Version kopiert werden:

  openai-to-cli.js:
    - tools-Feld wird als <system>-Block mit Tool-Schemas + klarer
      Anweisung "Antworte <tool_call name=...>{json}</tool_call>"
      in den Prompt injiziert
    - role=tool messages werden als <tool_result>-Blocks eingewoben
      → Claude sieht den ganzen Tool-Use-Loop
    - assistant tool_calls werden als <tool_call>-Bloecke
      re-serialisiert, damit History-Roundtrips funktionieren
    - Multimodal-content (Array von text-Parts) unveraendert
      unterstuetzt (Original-sed-Patch eingebaut)

  cli-to-openai.js:
    - parsed <tool_call name="X">{json}</tool_call> aus result.result
    - liefert OpenAI-konforme tool_calls + finish_reason=tool_calls
    - Pre-Tool-Text bleibt im content erhalten
    - normalizeModelName null-safe (Original-sed-Patch eingebaut)

docker-compose.yml: zwei sed-Patches die jetzt in den Files leben
sind raus, dafuer ein /proxy-patches:ro-Mount + zwei cp-Kommandos.

Smoke-Tests mit Node lokal alle gruen (single + multi tool_calls,
mit/ohne Pre-Text, History-Replay mit tool_result).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:42:05 +02:00
duffyduckandClaude Opus 4.7 56b7b0868a debug(brain): Proxy-Response loggen — finish_reason + raw-msg + tool_calls-Anzahl
Diagnose-Log um Trigger-Hang zu klaeren: warum legt ARIA keinen Timer
an, obwohl trigger_timer als Tool definiert ist? Wir loggen jetzt nach
jedem Proxy-Call:
  - finish_reason
  - alle Keys aus der message
  - tool_calls-Anzahl + content-Laenge
  - die rohe message (truncated 1500 chars)

So sehen wir ob der Proxy tool_calls leer liefert (Proxy schluckt
tools-Feld?), ob Claude ignoriert (Anthropic-Native-Format statt
OpenAI?), oder ob unser Dispatch falsch parsed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:34:12 +02:00
duffyduck 14026ee940 release: bump version to 0.1.2.7 2026-05-12 01:23:18 +02:00
duffyduckandClaude Opus 4.7 8fc65a986d docs: README + issue — Aktuelle-Zeit-Block + in_seconds dokumentiert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:22:14 +02:00
duffyduckandClaude Opus 4.7 e4fc3fa976 fix(brain): Timer in 2min funktioniert wieder — Zeit im Prompt + in_seconds-Param
ARIA wusste nicht wieviel Uhr es ist (kein Bash, kein Time-Tool, kein
Timestamp im System-Prompt) und konnte fires_at als ISO-UTC schlicht
nicht ausrechnen. Zwei Fixes:

1. prompts.py: build_time_section() injiziert UTC + lokale Zeit
   (Europa/Berlin Sommer/Winter-Heuristik) als '## Aktuelle Zeit'-Block
   oben in den System-Prompt. Hilft auch beim Einordnen von
   Watcher-Conditions wie hour_of_day == 8.

2. agent.py trigger_timer-Tool: neuer Parameter `in_seconds` als
   Alternative zu fires_at. Bei relativen Angaben ('in 2 Minuten')
   rechnet jetzt der Server den absoluten Timestamp aus — keine
   Rechnerei in der LLM noetig. fires_at bleibt fuer feste Termine.
   required nur noch name + message.

Diagnostic-API (/triggers/timer) bleibt absolute-only, da der Browser
selbst datetime hat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:21:23 +02:00
duffyduck 45f0b44db2 release: bump version to 0.1.2.6 2026-05-12 01:08:35 +02:00
duffyduckandClaude Opus 4.7 6b91dd94a3 docs: README + issue — Triggers, GPS-Tracking, Bug-Fixes nachgezogen
README: Trigger-Tab in Diagnostic-Tabs-Listen, App-Feature
"GPS-Tracking (kontinuierlich)". issue.md: neuer Block
"Triggers-System (Phase B Punkt 5)" + 8 frische Bug-Fixes
oben (agent_activity-Haenger, Such-Scroll, STT-Bubble-Timing,
Diagnostic Brain-Antworten, Brain-Card-Live-Status, App-Chat-Sync,
Konversation-Reset-Doppelschlag, OpenClaw-Ghost-IDs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:07:30 +02:00
duffyduckandClaude Opus 4.7 b3f1a32c8a feat(gps): kontinuierliches GPS-Tracking — Blitzer-Warner-Pipeline komplett
ARIA kann jetzt GPS-Watcher mit near() effektiv nutzen: die App liefert
kontinuierliche Position, Brain wertet sie in den Background-Triggers aus.

rvs/server.js
  ALLOWED_TYPES: location_update (App→Bridge) + location_tracking (Brain→App).

bridge/aria_bridge.py
  location_update Handler: persistiert {lat, lon} via _persist_location in
  /shared/state/location.json — selber Pfad wie chat/audio-events, aber als
  eigenes Event ohne Chat-Overhead.

aria-brain/agent.py
  Neues Meta-Tool request_location_tracking(on, reason). Dispatcher fuegt
  {type: "location_tracking", on, reason} zu _pending_events hinzu →
  Bridge forwarded als RVS-Message zur App.

aria-brain/prompts.py
  Trigger-Section bekam neuen Block "GPS-Watcher mit near()": ARIA wird
  angewiesen request_location_tracking(on=true) zu rufen wenn sie einen
  near()-Watcher anlegt, und wieder false beim Loeschen des letzten.

android/src/services/gpsTracking.ts (NEU)
  Singleton-Service. start(reason) → Geolocation.watchPosition mit
  distanceFilter 30m + interval 15s, sendet location_update an RVS.
  stop(reason) → clearWatch. Persistiert Status in 'aria_gps_tracking',
  restoreFromStorage() beim Settings-Mount. Permission-Request fuer
  ACCESS_FINE_LOCATION + Toast-Benachrichtigung bei An/Aus.

android/src/screens/SettingsScreen.tsx
  Neuer Switch im "Standort"-Block: "GPS-Tracking (kontinuierlich)" mit
  Hinweis-Text. Subscribe auf gpsTrackingService.onChange damit Toggle
  reflektiert wenn ARIA das per Tool umschaltet.
  RVS-Handler: location_tracking → gpsTrackingService.start/stop mit
  Reason aus Brain-Tool.

Ablauf Stefan→ARIA→Blitzer:
  1. Stefan: "Warn mich vor Blitzern auf Route nach Rhauderfehn"
  2. ARIA: skill_create("blitzer-warner") falls noch nicht da
  3. ARIA: run_blitzer-warner → Liste {lat,lon,name}
  4. ARIA: pro Eintrag trigger_watcher mit near(lat,lon,500)
  5. ARIA: request_location_tracking(on=true, reason="Blitzer-Warner aktiv")
  6. App: GPS-Tracking startet, sendet alle 15s location_update
  7. Bridge: /shared/state/location.json wird aktuell gehalten
  8. Brain-Background-Loop: alle 30s near()-Check pro Trigger
  9. Bei Erfolg: ARIA spricht "Blitzer A31 km 12 in 500m"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:02:05 +02:00
duffyduckandClaude Opus 4.7 27a20ff73f feat(brain): GPS-Variablen + near()-Helper + erweiterte Condition-Vars
ARIA kann jetzt GPS-basierte Watcher-Trigger anlegen (Blitzer-Warner-Use-Case),
plus erweiterte Time-, System- und Activity-Variablen.

bridge/aria_bridge.py
  _persist_state() schreibt atomar nach /shared/state/<key>.json.
  Bei jedem chat- und audio-Event:
    - location → /shared/state/location.json {lat, lon, ts_unix}
    - last_user_ts → /shared/state/activity.json
  Brain-Watcher lesen das fuer die GPS- und Activity-Variablen.

aria-brain/watcher.py — komplett ueberarbeitet
  Neue Variablen-Sets:
    GPS:       current_lat, current_lon, location_age_sec (-1 = nie gesehen)
    Zeit (+):  minute_of_hour, day_of_month, month, year, is_weekend, unix_timestamp
    System:    ram_free_mb (MemAvailable), cpu_load_1min (loadavg)
    Activity:  last_user_message_ago_sec
    Memory:    pinned_count (zusaetzlich zu memory_count)

  Neue Funktion fuer Conditions:
    near(lat, lon, radius_m)  Haversine-Distanz von current_lat/lon
                              zum Punkt. False wenn keine Position bekannt.

  Parser-Erweiterung:
    ast.Call jetzt erlaubt, ABER nur fuer direkte Funktionsnamen aus der
    Whitelist (_ALLOWED_FUNCTIONS = {"near"}). Keine Attribute-Access,
    keine Keywords, Args nur Constants/Names/UnaryOp.
  Selbsttest blockt korrekt:
    __import__("os")...           → "Funktionsaufruf nur ueber direkten Namen"
    memory_count.__class__         → "Verbotener Ausdruck: Attribute"
    (lambda: 1)()                  → "Funktionsaufruf nur ueber direkten Namen"

aria-brain/main.py
  /triggers/conditions liefert jetzt zusaetzlich {functions:[...]} mit
  Signaturen + Beschreibungen. current-Snapshot filtert callable() raus
  damit JSON serialisierbar bleibt.

aria-brain/prompts.py + agent.py
  build_triggers_section bekommt condition_funcs als 4tes Argument und
  listet die im System-Prompt unter "Verfuegbare Funktionen". Operatoren-
  Hinweis ergaenzt mit Beispielen + Regeln (keine Variablen in Funktions-
  Args, keine Schachtelung).

diagnostic/index.html
  Trigger-Create-Modal: Variablen-Info-Block zeigt jetzt sowohl Variablen
  (mit aktuellen Werten) als auch Funktionen (Signatur + Beschreibung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:52:13 +02:00
duffyduck cd8700101b release: bump version to 0.1.2.5 2026-05-12 00:40:25 +02:00
duffyduckandClaude Opus 4.7 f60db2f156 feat(brain+ui+app): Triggers — passive Aufweck-Quellen fuer ARIA
ARIA hatte bisher nur ein "User fragt → Brain antwortet"-Modell. Neu:
Trigger laufen passiv im Hintergrund (kein LLM-Call) und wecken ARIA
nur dann auf wenn ein Event tatsaechlich passiert.

Drei Typen, zwei aktuell implementiert:
  timer   — einmalig zu festem ISO-Timestamp ("erinner mich in 10min")
  watcher — Polling alle N Sek einer Condition, feuert bei True mit Throttle
            (z.B. "disk_free_gb < 5", max 1x/h)
  cron    — Platzhalter fuer spaeter

aria-brain/triggers.py
  CRUD auf /data/triggers/<name>.json + /data/triggers/logs/<name>.jsonl.
  create_timer, create_watcher, mark_fired, list_logs, etc.

aria-brain/watcher.py
  Built-in Condition-Variablen: disk_free_gb, disk_free_pct, uptime_sec,
  hour_of_day, day_of_week, rvs_connected, memory_count.
  Sicherer Condition-Parser via ast — Whitelist auf Vergleich + BoolOp +
  Name + Const. Kein eval, kein exec, keine Builtins.

aria-brain/background.py
  Async Loop laeuft alle 30s, sammelt einmalig Variables, geht durch
  Trigger-Liste, _should_fire-Check (Timer: fires_at vergangen / Watcher:
  check_interval + throttle respektiert + condition true). Fire ruft
  agent.chat(prompt, source="trigger") — ARIA bekommt das wie eine
  Push-Nachricht und antwortet via Bridge → RVS → App.

aria-brain/main.py
  /triggers/list, /{name}, /{name}/logs, /timer, /watcher, PATCH, DELETE,
  /triggers/conditions (Variablen + aktuelle Werte). Lifespan-Handler
  startet den Background-Loop beim Container-Start, stoppt beim Shutdown.

aria-brain/agent.py
  Meta-Tools fuer ARIA: trigger_timer, trigger_watcher, trigger_cancel,
  trigger_list. ARIA legt Trigger via Tool-Call selbst an wenn Stefan das
  wuenscht. Side-Channel-Event 'trigger_created' wird in chat-Response
  mitgeschickt damit App + Diagnostic eine Bubble zeigen.

aria-brain/prompts.py
  Neue System-Prompt-Section: Liste aktiver Triggers + verfuegbare
  Condition-Variablen mit aktuellen Werten + Operatoren-Erklaerung.
  ARIA weiss damit immer was es schon gibt und welche Vars sie nutzen kann.

bridge/aria_bridge.py + rvs/server.js
  trigger_created als neuer RVS-Message-Type, Bridge forwarded das aus
  data.events analog zu skill_created.

diagnostic/index.html
  Neuer Top-Tab "Trigger". Liste mit Type-Badges (⏱ TIMER / 👁 WATCHER),
  Status, Fire-Count, last_fired. Aktivieren/Deaktivieren + Löschen pro
  Trigger. "+ Neu"-Modal mit Type-Dropdown, Timer-Minuten oder
  Watcher-Condition + Vars-Anzeige + Throttle. Info-Modal-Eintrag mit
  Erklaerung. Live-Bubble im Chat wenn ARIA selbst einen anlegt.

android/src/screens/ChatScreen.tsx
  trigger_created RVS-Handler → eigene Bubble (gelber Border, " ARIA
  hat einen Trigger angelegt", Type/Detail/Message/Zeit). ChatMessage
  bekam triggerCreated-Feld. Lokal-only-Schutz beim Server-Sync analog
  zu skill_created.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:38:58 +02:00
duffyduckandClaude Opus 4.7 c8ed864a31 fix(diagnostic): ARIA-Textantworten landen jetzt im Chat (Gateway-Dedup raus)
Symptom: Diagnostic-Chat zeigt nur ARIA-Dateien (file_from_aria), Text-
Antworten kamen nicht an. STT-Eintraege + User-Messages waren sichtbar.

Ursache: Im rvs_chat-Handler stand
  if (sender === 'aria') return;
Die alte Begruendung war "ARIA-Antworten kommen schon via Gateway (chat:final)".
Das galt zu OpenClaw-Zeit, wo Diagnostic eine direkte WS zum aria-core hatte.
Gateway ist seit dem Abriss weg, ARIA-Antworten kommen jetzt ausschliesslich
via RVS → der return blockte sie still.

Fix: chatType + label je nach sender:
  - aria  → received-Bubble, Label "ARIA"
  - stt   → sent-Bubble, Label "🎤 Spracheingabe" (wie vorher)
  - sonst → sent-Bubble, Label "via RVS (<sender>)"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:21:10 +02:00
duffyduckandClaude Opus 4.7 71885bd22f fix: 3 Bugs — agent_activity haengt, Such-Scroll, STT-Bubble-Timing
Bug 1: "ARIA denkt..." in der App bleibt stehen
  _process_core_response setzte am Ende kein idle — die alten Aufrufe waren
  in der OpenClaw-WS-Loop, in der Brain-HTTP-Variante fehlten sie. Plus
  send_to_core schickte agent_activity direkt via _send_to_rvs ohne den
  _last_activity_state-Cache zu pflegen → _emit_activity("idle") wurde
  spaeter dedupliziert.
  Fix:
    - _emit_activity statt direktem _send_to_rvs fuer thinking
    - _emit_activity("idle") am Ende von _process_core_response
    - _last_chat_final_at bewusst NICHT setzen — die 3s-Cooldown war fuer
      trailing OpenClaw-Events, wuerde bei Voice die naechste thinking-Welle
      unterdruecken

Bug 2: App Chat-Suche scrollt nicht zur Stelle
  scrollToIndex wurde zu fruh aufgerufen (Layout noch nicht fertig) und
  viewPosition: 0.4 in inverted-FlatList war ungenau.
  Fix:
    - requestAnimationFrame um den Scroll-Aufruf
    - viewPosition: 0.5 (mittig)
    - onScrollToIndexFailed: erst grob scrollen via averageItemLength,
      dann nach 250ms praeziser nachfassen

Bug 3: Voice-Bubble bekommt STT-Text erst mit ARIA-Antwort
  _process_app_audio rief erst send_to_core (blockt synchron auf Brain,
  kann 300s dauern), DANN STT-Broadcast. App sah den eigenen Text erst
  wenn ARIA fertig war.
  Fix: Reihenfolge getauscht — STT-Broadcast zuerst, dann send_to_core.
  Voice-Bubble bekommt jetzt den erkannten Text sofort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:17:10 +02:00
duffyduck 150ca9055f release: bump version to 0.1.2.4 2026-05-11 23:57:16 +02:00
duffyduckandClaude Opus 4.7 a43b5dea48 docs: README + issue.md — App-Chat-Sync-Verhalten praezisiert
App-Chat-Sync ist seit Commit 039b401 "Server is Source of Truth" — bei
jedem Reconnect KOMPLETTER Server-Stand statt incremental. Doku angepasst:

  - App leert sich wenn Server leer ist (z.B. nach "Konversation zuruecksetzen")
  - Lokal-only Bubbles bleiben erhalten (Skill-Notifications, Voice ohne STT)
  - Bridge schreibt chat_backup.jsonl pro Turn — als Server-Backing-Store

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:56:14 +02:00
duffyduckandClaude Opus 4.7 039b401e6f fix(app): kompletter Server-Sync bei Reconnect — Server ist Source of Truth
Symptom: Diagnostic-Server hat leere Chat-History (z.B. nach "Konversation
zuruecksetzen" oder Wipe), App zeigt aber weiterhin ihren alten lokalen
Stand. Wer das Wipe-Event verpasst hat (App offline), bleibt veraltet.

Ursache: App schickte beim Reconnect chat_history_request {since: lastSync}
und ignorierte leere Antworten. Wenn der Server ueberhaupt nichts mehr hat
liefert er korrekt [] zurueck — App behielt aber lokalen State.

Fix:
  - App schickt jetzt {since: 0, limit: 200} → KOMPLETTER Server-Stand
  - Handler ersetzt die persistierte Chat-History mit dem Server-Stand
    (statt zu mergen)
  - Lokal-only Bubbles bleiben erhalten:
      * Skill-Created-Notifications (skillCreated gesetzt)
      * Laufende Sprachnachrichten ohne STT-Result (audioRequestId gesetzt
        und text leer/Placeholder)
  - Wenn Server leer: lastSync ebenfalls geloescht (sauberer Restart-State)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:55:25 +02:00
duffyduck 5181f53052 release: bump version to 0.1.2.3 2026-05-11 23:48:00 +02:00
duffyduckandClaude Opus 4.7 3ddd888b72 docs: README + issue.md mit Token-Metrics + Diagnostic-Tabs aktualisiert
README.md
  - Phase B erledigt-Liste um zwei Punkte erweitert:
    Token/Call-Metrics + Multi-Select-Datei-Manager
  - Diagnostic-Tabs in der Installations-Sektion aktualisiert
    (5 Tabs statt 4 — Skills als eigener Tab)
  - Diagnostic-Sektion weiter unten: Tabs-Liste komplett ueberholt,
    Token-Metrics als eigener Bullet in "Was zusaetzlich drin steckt"

issue.md
  - "Token/Call-Metrics" als erledigter Punkt in "Diagnostic/App-Features"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:45:37 +02:00
duffyduckandClaude Opus 4.7 18730763d8 feat(brain+diagnostic): Token/Call-Metrics mit Subscription-Plan-Tracking
Stefan hat den Max 5x Plan (~\$90-100/Monat), ungefaehres Limit 225 Calls pro
5h-Fenster fuer Sonnet. Damit nicht in eine Tool-Loop-Schleife laufen ohne
es zu merken: kleine Metrics-Pipeline, sichtbar in der Diagnostic.

aria-brain/metrics.py
  Append-only JSONL Logger unter /data/metrics.jsonl. Pro Claude-Call eine
  Zeile {ts, model, in, out} mit Token-Schaetzung (chars/4, Anthropic-
  Heuristik). aggregate(window) zaehlt die letzten N Sekunden.
  Auto-Rotate bei 50k Zeilen → 25k behalten (~70 KB/Monat bei 1k Calls/Tag,
  Cap also weit oben).

aria-brain/proxy_client.py
  chat_full() ruft am Ende metrics.log_call(model, messages_in, reply).
  Failed/exception-Pfade loggen nicht (sonst false positives).

aria-brain/main.py
  GET /metrics/calls → {h1, h5, h24, d30}, jedes Window mit calls,
  tokens_in, tokens_out, by_model.

diagnostic/index.html
  Neue Card "Token / Calls" im Gehirn-Tab. Plan-Dropdown
  (Pro / Max 5x / Max 20x / Custom), localStorage-persistiert. 4 Metric-
  Zellen fuer 1h/5h/24h/30d mit Calls + Tokens. Progress-Bar oben zeigt
  5h-Counter gegen Plan-Limit. Warn-Klassen: gelb bei 80%, rot bei 90%.
  Auto-Refresh alle 30s wenn Gehirn-Tab offen, plus bei Tab-Wechsel.
  Info-Modal erklaert die Limits + dass HTTP-Call != User-Frage (Tool-Use
  kann pro Frage bis zu 8 Calls verursachen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:43:56 +02:00
duffyduckandClaude Opus 4.7 93a193a572 docs: README + issue.md — Phase B komplett (Punkte 2/3/4 + drumherum)
README.md
  - Phase A erledigt-Liste erweitert: Info-Buttons, Multi-Select+ZIP, App-Datei-Manager
  - Neue Sektion "Phase B — Brain mit Memory + Loop + Skills" mit allen 3
    Hauptpunkten (Migration / Conversation-Loop / Skills-System) abgehakt
  - Plus drumherum: Sprachmodell-Setting, App-Chat-Sync, Chat-Suche Next/Prev
  - Phase 2: "Erste Skills bauen lassen" — primaerer naechster Schritt

issue.md
  - "Brain — Phase B (komplett)" Sektion oben — strukturiert in
    Infrastruktur / Memory / Conversation-Loop / Skills / Diagnostic+App
  - Alle Phase-B-Punkte abgehakt, mit Detail-Bullets
  - Offen-Liste: Tool-Use-Verifikation (claude-max-api-proxy tools-Param)
    explizit als ToDo, plus "Erste Skills bauen lassen" als Use-Case

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:36:50 +02:00
duffyduckandClaude Opus 4.7 b268559884 feat: App-Chat-Suche mit Next/Prev + Diagnostic Sprachausgabe-Layout
App Chat-Suche umgebaut von Filter zu Highlight+Navigation
  Vorher: searchQuery filtert die FlatList, zeigt nur Treffer.
  Jetzt:  Suche filtert NICHT mehr, alle Nachrichten bleiben sichtbar.
          Treffer wird gelb (FFD60A) umrandet, FlatList scrollt automatisch
          dorthin.
  - Suchleiste: Input + Counter "N/M" + ▲ + ▼ + ✕
  - ▲ / ▼ navigieren chronologisch durch alle Matches (zyklisch)
  - searchMatchIds via useMemo, searchIndex separates State
  - scrollToIndex mit viewPosition: 0.4 (Treffer landet im oberen Drittel)
  - onScrollToIndexFailed Fallback nach 200ms (Layout noch nicht fertig)

Diagnostic Sprachausgabe-Layout
  Export/Import-Buttons wandern aus dem Section-Header in den Details-Block
  neben "Anwenden" (Stefan's Wunsch). Header zeigt nur noch den Titel.
  File-Input bleibt versteckt im Section-Top, wird vom neuen Button-Block
  unten ueber click() getriggert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:35:02 +02:00
duffyduckandClaude Opus 4.7 583e1b5e44 feat: Datei-Manager Multi-Select + Bulk-Download (ZIP) + Bulk-Delete
Diagnostic + App bekommen Mehrfach-Auswahl im Datei-Manager. Mehr als eine
Datei ausgewaehlt → Download als ZIP. Genau eine ausgewaehlt → einzeln.
Bulk-Delete loescht alle markierten in einem Rutsch.

diagnostic/Dockerfile
  zip via apk add — fuer das ZIP-Streaming im /api/files-download-zip.

diagnostic/server.js
  POST /api/files-download-zip   Body: {paths:[...]} → spawnt 'zip -j -q -',
                                  Pipes stdout in Response. Whitelist auf
                                  /shared/uploads/.
  POST /api/files-delete-batch   Body: {paths:[...]} → loescht alle, broadcastet
                                  file_deleted pro Pfad an Browser + RVS.

diagnostic/index.html
  filesSelected Set + Checkbox-UI pro Datei + "Alle markieren". Wenn 2+
  ausgewaehlt: POST an /api/files-download-zip, Browser saugt das als
  Blob runter. Bei 1: normaler Single-Download.

bridge/aria_bridge.py
  file_delete_batch_request    → ruft Diagnostic /api/files-delete-batch,
                                 antwortet mit file_delete_batch_response.
  file_zip_request {paths,reqId} → ruft Diagnostic /api/files-download-zip,
                                   base64-kodiert, capped auf 30 MB,
                                   sendet file_zip_response.

rvs/server.js
  ALLOWED_TYPES: file_delete_batch_request/response, file_zip_request/response.

android/src/screens/SettingsScreen.tsx
  fileManagerSelected Set + Checkbox-UI pro Datei + "Alle markieren"-Zeile
  oben. Bulk-Bar oben mit count, "⬇ ZIP" / "⬇ Download" (je nach Anzahl),
  und "🗑 Löschen". ZIP-Response landet base64 → RNFS in Downloads-Folder
  (aria-files-<timestamp>.zip), Toast mit Pfad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:30:40 +02:00
duffyduckandClaude Opus 4.7 ad64af2e17 feat: App-Chat-Sync — verpasste Nachrichten + chat_cleared Live-Update
Zwei zusammenhaengende Bugs:
  1. App aktualisierte nicht wenn die Diagnostic "Konversation komplett
     zuruecksetzen" gedrueckt hat — die App hatte den lokalen Stand weiter
  2. Nachrichten die kamen waehrend die App offline/geschlossen war,
     wurden nicht nachgeladen

Loesung: chat_backup.jsonl wird wieder geschrieben (Bridge statt Diagnostic,
weil OpenClaw-Code-Pfad tot ist) und dient als Server-Truth fuer App+Diagnostic.

bridge/aria_bridge.py
  _append_chat_backup() schreibt jeden Turn (User + ARIA) als JSONL-Zeile
  in /shared/config/chat_backup.jsonl. Trigger: send_to_core (User) +
  _process_core_response (Assistant, inkl. file-Attachments).

  _read_chat_backup_since(since_ms, limit) liest die Datei, filtert auf
  ts > since_ms, gibt max limit neueste zurueck. Honoriert file_deleted-Marker.

  Neuer RVS-Handler chat_history_request {since, limit?} → antwortet mit
  chat_history_response {messages: [...], since}.

diagnostic/server.js
  /api/chat-history-clear broadcastet jetzt zusaetzlich chat_cleared via
  RVS (sendToRVS_raw), damit App ihre lokale Liste auch leert. Vorher nur
  Browser-Clients via broadcast() — App war aussen vor.

rvs/server.js
  ALLOWED_TYPES um chat_history_request, chat_history_response, chat_cleared.

android/src/screens/ChatScreen.tsx
  - Bei (re)connect: AsyncStorage 'aria_chat_last_sync' lesen → send
    chat_history_request {since}
  - Handler chat_history_response: incoming → ChatMessage[] mappen,
    Attachments aus 'files'-Array rekonstruieren, mergen (Dedup via timestamp),
    lastSync hochziehen
  - Handler chat_cleared: setMessages([]) + AsyncStorage 'chat_messages' +
    'last_sync' weg
  - Bei jeder eingehenden chat-Message: 'aria_chat_last_sync' updaten damit
    Reconnect nicht doppelt nachzieht

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:24:52 +02:00
duffyduckandClaude Opus 4.7 0f52f8c74e fix(diagnostic): loadBrainStatus updated jetzt beide Cards (Main + Gehirn)
Symptom: Main-Tab "ARIA BRAIN"-Card blieb auf "Lade..." haengen. Klick auf
"Status pruefen" tat scheinbar nichts.

Ursache: loadBrainStatus() suchte nur brain-status (Gehirn-Tab). Die Card
im Main-Tab hat aber andere IDs (brain-dot + brain-status-short + brain-error),
die wurden nirgends mehr befuellt seit updateState() das nicht mehr macht.

Fix: loadBrainStatus update jetzt BEIDE Anzeigen synchron — kompakte Main-Card
mit Dot/Status/Error UND die ausfuehrliche Gehirn-Tab-Zeile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:07:42 +02:00
duffyduckandClaude Opus 4.7 c6761f9830 feat(diagnostic): Info-Buttons mit Modal-Erklaerungen im Gehirn-Tab
Reusable Info-Modal-System: kleines (ℹ)-Button neben Ueberschriften, beim
Klick oeffnet ein Modal mit ausfuehrlicher Erklaerung. Fuer die 4 wichtigsten
Brain-Konzepte sind die Texte vor-definiert (INFO_TEXTS dict).

  - Gehirn — Status     online/offline, N Memories, Qdrant-Endpoint
  - Konversation        Rolling Window 50, Schwelle 60, Destillat-Logik,
                        Hinweis warum chat_backup ≠ conversation.jsonl
  - Memories            Hot vs. Cold, alle 8 Typen erklaert, semantische Suche
  - Bootstrap           Die drei Wege (Migration / Snapshot / Komplett-Gehirn)

Plus inline-ℹ-Button neben der "Konversation: N Turns"-Zeile in der
Status-Card, damit man dort wo's relevant ist sofort die Erklaerung findet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:03:47 +02:00
duffyduckandClaude Opus 4.7 89185e3d4d fix(diagnostic): Brain-Card live + Runtime-Konfig-Text + Konversation-Reset macht beides
Drei zusammenhaengende Fixes — alle aus der OpenClaw-Umstellung uebrig.

Brain-Card im Main-Tab
  - updateState() schrieb "aria-core entfernt — Brain-Loop in Arbeit" in
    die Card, weil state.gateway noch den alten OpenClaw-Status-Text
    transportiert hat. Brain ist online, sah aber auf der Diagnostic
    aus als waere alles tot.
  - Fix: updateState laesst die Brain-Card jetzt in Ruhe. Daten kommen
    exklusiv von loadBrainStatus() (fetch /api/brain/health).
  - WS-onopen ruft jetzt loadBrainStatus(), plus 15s-Interval fuer
    Live-Refresh.

Runtime-Konfiguration
  - Hinweis-Text erwaehnte noch aria.env + "Bridge-Container neu starten".
    aria.env gibt's nicht mehr, und Brain liest die Config auch.
  - Neu: ".env / Bridge UND Brain lesen sie beim Start — nach Aenderung
    den jeweiligen Container neu starten (Reparatur-Section oben)."

Konversation-Reset
  - Vorher zwei verschiedene Konversations-Stores ohne klare UI:
    chat_backup.jsonl (Diagnostic-Anzeige) und conversation.jsonl
    (Brain Rolling Window). "Konversation leeren" leerte nur Brain,
    die UI zeigte alle Turns weiter → verwirrend.
  - Neu: ein Button "🧹 Konversation komplett zurücksetzen" leert
    beides parallel (Promise.all auf /api/brain/conversation/reset
    + /api/chat-history-clear) plus die lokale Chat-View.
  - Erklaerungstext darunter macht klar was passiert + was bleibt
    (destillierte Facts + Memories in der Vector-DB).
  - Neuer Endpoint /api/chat-history-clear loescht chat_backup.jsonl
    und broadcastet leere chat_history an alle Browser-Clients.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:59:35 +02:00
duffyduckandClaude Opus 4.7 638c142513 fix(diagnostic): JS-Crash beim Laden durch Ghost-IDs aus OpenClaw-Zeit
Die Diagnostic-Seite lud nicht mehr richtig: bei jedem state-Update vom
Server crashte updateState() auf null.className weil 'gw-dot' nicht mehr
existiert (Gateway-Card wurde durch Brain-Card ersetzt). Mehrere weitere
Stellen waren ebenfalls auf nicht mehr existierende Elemente geleitet.

Bereinigt:
  - updateState() nutzt jetzt brain-dot/short/error (null-safe)
  - openCoreTerminal entfernt (aria-core ist raus)
  - closeTermModal null-safe fuer btn-core-term
  - 'core_auth' WS-Event entfernt
  - 'session_restarted' WS-Event entfernt (perms-status-Element war auch raus)
  - 'openclaw_config' WS-Event entfernt
  - rc-compact-after read/write aus loadRuntimeConfig/saveRuntimeConfig raus
    (Compact-After-Messages-Setting wurde mit aria-core entfernt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:53:30 +02:00
duffyduck 1f42b48d7f release: bump version to 0.1.2.2 2026-05-11 22:38:45 +02:00
duffyduckandClaude Opus 4.7 808174fafd fix(diagnostic+brain): Sprachmodell-Einstellung auf runtime.json umgestellt
War kaputt nach OpenClaw-Abriss: handleGetModel/handleSetModel haben gegen
aria-core (dockerExec + node-script in den Container) gearbeitet, der gibt's
nicht mehr.

diagnostic/server.js
  - handleGetModel/handleSetModel lesen/schreiben jetzt brainModel in
    /shared/config/runtime.json
  - RUNTIME_CONFIG_FIELDS um "brainModel" erweitert
  - Tote Variante (findSettingsFile + base64-node-script) komplett raus

aria-brain/proxy_client.py
  - Liest brainModel aus runtime.json beim Container-Start
  - Fallback: BRAIN_MODEL env → "claude-sonnet-4" Default
  - Bei Aenderung in Diagnostic: aria-brain restarten damit's greift
    (Hinweis steht in der UI)

diagnostic/index.html
  - Section "Model" → "Sprachmodell (Brain)"
  - Hinweis-Block mit Default-Erklaerung und Restart-Hinweis
  - Modelle: claude-sonnet-4 (default), claude-opus-4, claude-haiku-4-5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:36:14 +02:00
duffyduckandClaude Opus 4.7 ecc2bfd4a7 chore: OpenClaw-Reste raus — tools/, docs/tool-permissions, Diagnostic-Permissions-Section
tools/export-jsonl-to-md.js
  CLI-Konverter fuer OpenClaw-Session-JSONL zu Markdown. Mit OpenClaw raus.
  tools/-Ordner ist jetzt leer und auch entfernt.

aria-data/docs/tool-permissions.md
  Geschichts-Dokument zum OpenClaw-Tool-Permission-Bug. Nicht mehr relevant —
  Brain hat eigenes Tool-Use-System (Skills + Meta-Tools), keine granulare
  Permission-Maske. aria-data/docs/ Ordner war leer, mit raus.

diagnostic/index.html
  - "Tool-Berechtigungen"-Section aus den Einstellungen raus. War nur ein
    disabled Info-Toggle aus OpenClaw-Zeit ("Claude darf alle Tools").
    Brain weiss zur Laufzeit selbst welche Tools es hat (META_TOOLS +
    pro aktivem Skill ein dynamisches run_<skill>).
  - .perm-grid / .perm-item / .perm-info CSS-Klassen entfernt (sonst nirgends
    referenziert).

issue.md
  - tools/export-jsonl-to-md.js-Eintrag mit "(mit OpenClaw raus)" markiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:29:59 +02:00
duffyduckandClaude Opus 4.7 4fe6eb1b1e chore: Setup-Scripts auf neue Brain-Architektur, OpenClaw-Tutorial raus
aria-setup.sh
  Von 142 Zeilen auf 50 zurechtgestutzt. Alle OpenClaw-Schritte raus
  (openclaw.json, exec-approvals, Permissions fuer .openclaw, doctor).
  Es bleibt nur der SSH-Key-Setup fuer aria-wohnung — Brain + Proxy
  teilen sich denselben Key via Bind-Mount aria-data/ssh/.

init.sh
  Legt jetzt aria-data/brain/data + aria-data/brain/qdrant an damit die
  Bind-Mounts beim ersten docker compose up nicht ins Leere zeigen.

OpenClaw Tutorial _ MI.pdf
  Geloescht — OpenClaw ist abgerissen, das Tutorial gehoert nicht
  mehr ins Repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:25:32 +02:00
duffyduckandClaude Opus 4.7 0a3d43d531 docs: README + issue.md auf die neue Brain-Architektur
README.md
  - Architektur-Diagramm: aria-core (OpenClaw) raus, qdrant + brain rein
  - Tabelle der Deployments aktualisiert
  - .env-Beschreibung: minimal, nur Tokens + RVS, Rest lebt in
    runtime.json (gepflegt ueber Diagnostic)
  - aria-setup.sh-Beschreibung: nur noch SSH-Key-Setup
  - Datenverzeichnis-Baum: brain/qdrant + brain/data + brain-import
  - Docker-Volumes-Tabelle neu (Bind-Mounts statt named Volumes)
  - Diagnostic-Tabs (Main, Gehirn, Dateien, Einstellungen) ersetzen
    die alte Feature-Liste
  - Backup/Reset-Sektion: drei Wiederherstellungswege (brain-import
    Migration, Bootstrap-Snapshot, komplettes Gehirn)
  - Container-Restart statt doctor/aria-restart
  - docker-Befehle: brain/qdrant statt aria-core
  - Phase A erledigt-Liste + Phase B Status (Punkte 1-4 erledigt:
    Brain-Skeleton, Migration, Conversation-Loop, Skills-System)

issue.md
  - "Brain (Phase B)"-Sektion ganz oben unter Offen
  - 9 erledigte Refactor-Punkte abgehakt
  - Aria-core-spezifische TODOs entfernt (Compact-Auto, Session-Reset,
    alle-.env-via-Diagnostic war eh schon erledigt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:24:53 +02:00
duffyduckandClaude Opus 4.7 4c94fb1eb4 feat(xtts): Voice Export/Import auf der Gamebox
Zwei neue RVS-Handler in der F5-TTS-Bridge:

  xtts_export_voice
    Packt <name>.wav + <name>.txt aus VOICES_DIR als tar.gz in-memory,
    sendet base64-codiert als xtts_voice_exported zurueck. Diagnostic
    baut daraus den Browser-Download.

  xtts_import_voice
    Empfaengt base64 tar.gz mit <name>.wav (+ optional <name>.txt),
    legt sie in VOICES_DIR ab (sanitized name, Path-Traversal-Schutz),
    sendet xtts_voice_imported zurueck. Anschliessend handle_list_voices
    damit App/Diagnostic die neue Stimme sofort sehen.

So koennen Stimmen zwischen mehreren Gameboxen wandern, ohne die
WAV-Files manuell rumzukopieren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:24:35 +02:00
duffyduckandClaude Opus 4.7 1a819c62da feat(rvs): neue Message-Types fuer Brain-Flow
ALLOWED_TYPES erweitert um:

  - container_restart                   (App/Diagnostic kann gezielt
                                         Bridge/Brain/Qdrant restarten)
  - file_list_request, file_list_response,
    file_delete_request, file_deleted    (Datei-Manager in App + Diagnostic)
  - xtts_export_voice, xtts_voice_exported,
    xtts_import_voice, xtts_voice_imported (Voice-Backup zwischen
                                            Gameboxen via RVS)
  - skill_created                        (Brain → Bridge → App/Diagnostic:
                                          ARIA hat einen Skill angelegt)

Entfernt:

  - doctor_fix, aria_restart, aria_session_reset
    (aria-core ist abgerissen — durch container_restart ersetzt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:24:27 +02:00
duffyduckandClaude Opus 4.7 bebd437abc feat(app): Datei-Manager, Skill-Created-Bubble, Zoom rewriten, Repair-Cleanup
Drei groessere Aenderungen in der Android-App.

Datei-Manager (Settings → Dateien)
  - Neuer Eintrag im Settings-Menue → Modal mit Liste
  - Suche + Filter (Alle / Von ARIA / Vom User)
  - Per Eintrag: ARIA/USER-Badge, Groesse, Datum, Loeschen-Button
  - file_list_request via RVS → Bridge → Diagnostic-HTTP → response
  - file_delete_request loescht serverseitig, file_deleted-Event
    aktualisiert ALLE Chat-Bubbles (Attachment.deleted = true mit
    Strikethrough-Name + 🗑️-Icon)

Skill-Created-Bubble
  - Neuer ChatMessage.skillCreated Typ — eigenes Render mit gelbem
    Border, Skill-Name, Beschreibung, Execution-Mode, Active-Status
  - Falls Skill-Setup fehlschlug: ⚠ Setup-Fehler-Zeile direkt in der Bubble
  - Stefan sieht in der Chat-History immer wenn ARIA selbst einen
    Skill angelegt hat — Transparenz statt schweigend im Hintergrund

Pinch-Zoom rewriten (ZoomableImage.tsx)
  - Multi-Touch-Race-Bugs in der alten Variante geloest:
    * Touch-Count jetzt aus e.nativeEvent.touches.length statt
      gestureState.numberActiveTouches (war nicht zuverlaessig)
    * Re-Snapshot bei JEDEM Finger-Wechsel (1↔2) → keine Spruenge mehr
    * Doppel-Tap via onPanResponderRelease + Bewegungs-Cap
    * pointerEvents="none" auf Image-Wrapper → Touches gehen garantiert
      an PanResponder-View
    * collapsable={false} verhindert Android-View-Flattening
  - 2-Finger-Pinch 1x..5x, simultaner Pan via Focal,
    1-Finger-Pan nur wenn gezoomt (>1.02x), Doppel-Tap toggelt 1x↔2.5x

App SettingsScreen Repair-Section
  - aria-core-spezifische Buttons raus: 🔧 Reparieren, 🚨 ARIA hart neu,
    🧹 Konversation komprimieren (OpenClaw ist abgerissen)
  - Stattdessen generischer container_restart fuer aria-bridge/brain/qdrant
  - Repair-Buttons aus der "ARIA denkt..."-Bubble entfernt (nur Abbrechen)

ChatScreen
  - skill_created und file_deleted Handler im RVS-Message-Switch
  - file_list_response (Modal-State liegt in SettingsScreen)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:24:06 +02:00
duffyduckandClaude Opus 4.7 5dae0a401c feat(diagnostic): Gehirn-Tab, Skills-Tab, Datei-Manager, Wipe-All
Komplett-Umbau der Diagnostic auf die neue Brain-Architektur. OpenClaw-spezifische
Sektionen raus (Gateway-Card, Watchdog, OpenClaw-Config, doctor-fix/Restart-Buttons,
Sessions-Verwaltung, Compact-After-Messages), dafuer drei neue Tabs.

Neue Tabs
  - Gehirn   Memory-CRUD (Add/Edit/Delete + Such-Feld + Type/Pinned-Filter),
             Conversation-Status mit Distill-Trigger + Reset,
             Bootstrap & Migration: aus brain-import/ migrieren,
             Bootstrap-Export/Import als JSON (nur pinned),
             komplettes Gehirn als tar.gz exportieren/importieren.
  - Skills   Aufklappbare Liste mit Run-Logs, Aktivieren/Deaktivieren,
             Export pro Skill (tar.gz), globaler Import-Button,
             "von ARIA"-Badge wenn vom Agent selbst angelegt.
  - Dateien  Browser fuer /shared/uploads/ — User+ARIA-Dateien herunterladen
             oder loeschen. Beim Delete: Live-Update der Chat-Bubbles
             (durchgestrichener Pfad, kein Download-Link mehr).

Einstellungen
  - Neue "Reparatur & Restart"-Section mit Container-Restart-Buttons
    (Bridge/Brain/Qdrant) ueber generischen /api/container-restart
  - Komplett-Reset (🗑 ALLES loeschen) — Brain + Qdrant stoppen,
    /shared/config + /shared/voices + /brain/data + /brain/qdrant leeren
  - Sprachausgabe-Header: Voice-Config-Bundle Export/Import (JSON)
  - Voice-Liste: ⬇ pro Stimme + ⬆ Stimme importieren (tar.gz via XTTS-Bridge)

Backend (server.js)
  - connectGateway/Watchdog/checkGatewayHealth: No-Op (aria-core ist raus)
  - handleLoadChatHistory neu — liest jetzt chat_backup.jsonl statt
    OpenClaw-Sessions; respektiert file_deleted-Marker
  - Neue Endpoints: /api/container-restart, /api/wipe-all,
    /api/files-list, /api/files-download, /api/files-delete,
    /api/voice-config-export, /api/voice-config-import,
    /api/brain/* (Reverse-Proxy zum aria-brain:8080)
  - Entfernt: /api/doctor-fix, /api/aria-restart, /api/aria-session-reset,
    /api/sessions-snapshot, handleGetOpenClawConfig

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:23:42 +02:00
duffyduckandClaude Opus 4.7 dfe0b55e3e feat(brain): Phase B — Vector-DB-Memory, Conversation-Loop, Skills, Tool-Use
OpenClaw (aria-core) ist raus, ARIA laeuft jetzt mit eigenem Agent-Framework
im aria-brain Container. Vector-DB-basiertes Gedaechtnis statt Sessions,
eigener Conversation-Loop mit Hot+Cold-Memory + Rolling Window, Tool-Use
fuer Skills, Memory-Destillat-Pipeline.

aria-brain/ (neuer Container)
  - main.py            FastAPI auf 8080, alle Endpoints
  - agent.py           Conversation-Loop mit Tool-Use (skill_create + run_<skill>)
  - conversation.py    Rolling Window, JSONL-Persistenz, Distill-Marker
  - proxy_client.py    httpx-Wrapper zum Claude-Proxy, OpenAI-Format
  - prompts.py         System-Prompt aus Hot+Cold+Skills
  - migration.py       Markdown-Parser fuer brain-import/ → atomare Memories
  - skills.py          Filesystem-Layer fuer /data/skills/<name>/ (Python-only,
                       venv pro Skill, tar.gz Export/Import, Run-Logs)
  - memory/            Embedder (sentence-transformers, multilingual MiniLM)
                       + VectorStore (Qdrant-Wrapper)

docker-compose.yml
  - aria-core (OpenClaw) raus, openclaw-config Volume raus
  - aria-brain Service (FastAPI + Memory)
  - aria-qdrant Service (Vector-DB) mit Bind-Mount aria-data/brain/qdrant/
  - Diagnostic teilt jetzt Netzwerk mit Bridge (vorher: aria-core)
  - Brain bekommt SSH-Mount fuer aria-wohnung + /import fuer brain-import/

bridge/aria_bridge.py
  - send_to_core → HTTP-Call an aria-brain:8080/chat (statt OpenClaw-WS)
  - aria-core-spezifische Handler raus: doctor_fix, aria_restart,
    aria_session_reset, Auto-Compact-Logik, OpenClaw-Handshake
  - Generischer container_restart-Handler (Whitelist Bridge/Brain/Qdrant)
  - Side-Channel-Events aus /chat-Response (z.B. skill_created) werden
    als RVS-Events forwarded
  - file_list_request / file_delete_request → an Diagnostic forwarded
  - Tote OpenClaw-Connection-Logik bleibt im Code als Referenz (nicht aktiv)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:23:17 +02:00
duffyduckandClaude Opus 4.7 ec8e4563a5 chore(gitignore): OpenClaw-Reste raus, Brain-Struktur rein
- aria-data/config/*.env + openclaw.env-Whitelist raus (existieren nicht mehr)
- aria-data/config/USER.md → aria-data/brain-import/USER.md (neuer Ort)
- aria-data/brain/ → aria-data/brain/data/ + qdrant/ (Bind-Mounts fuer Brain + Qdrant)
- aria-data/config/diag-state/ ergaenzt (Diagnostic persistenter State)
- aria-data/voices/ raus (Stimmen liegen auf der Gamebox, nicht auf der VM)
- bridge/__pycache__/ raus (vom globalen __pycache__/ schon abgedeckt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:22:49 +02:00
duffyduckandClaude Opus 4.7 7d4fe293b5 chore(phase-a): aria.env + OpenClaw-specific configs raus, Brain-Backup angelegt
Vorbereitung fuer Bridge-als-Agent (OpenClaw raus). Phase-A-Cleanup:

- aria-data/brain-import/ angelegt — System-Prompt-Files BOOTSTRAP.md
  + AGENT.md + USER.md.example + TOOLING.md.example dort archiviert.
  Werden vom neuen Agent-Framework spaeter importiert.
- aria-data/config/aria.env + .example geloescht (alle Eintraege waren
  tot oder via runtime.json/Hardcode-Default abgedeckt)
- aria-data/config/openclaw.env + openclaw-auth.json geloescht
  (Provider-Config landet im Bridge-Code)
- docker-compose: AGENT/BOOTSTRAP/USER/openclaw.env-Mounts aus aria-core
  raus; aria.env-Mount aus bridge raus; COMPACT_AFTER_MESSAGES env raus
  (kommt jetzt aus runtime.json)
- bridge: CONFIG_PATH-Lesen aus /config/aria.env entfernt, load_config
  nutzt nur noch runtime.json; _compact_after liest compactAfterMessages
  aus runtime.json (Default 140)
- diagnostic: Eingabefeld "Compact nach Messages" in Runtime-Config-UI;
  load/save schreiben/lesen compactAfterMessages
- init.sh: nur noch .env-Bootstrap (Rest landet eh in runtime.json/DB)

ARIA antwortet im Phase-A-Status ohne ihre Persoenlichkeit
(System-Prompt nicht mehr gemountet, "raw Claude" durch Proxy) —
das ist Absicht und Uebergang bis das neue Gehirn live ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:13:54 +02:00
duffyduckandClaude Opus 4.7 3a6bce72c6 fix(diagnostic): logBoxes.tts entfernt — Element gibt's nicht mehr, null.addEventListener crashte das ganze JS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:44:23 +02:00
duffyduckandClaude Opus 4.7 685c5ffb55 rename(diagnostic): Pipeline-Tab → Trace (End-to-End-Mitschnitt einer Anfrage)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:41:54 +02:00
duffyduckandClaude Opus 4.7 ed99775d69 chore(diagnostic): TTS-Diagnose-Tab raus (Voice-Settings sind eh in Einstellungen)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:37:35 +02:00
duffyduck b57abc67ba release: bump version to 0.1.2.1 2026-05-11 19:28:32 +02:00
duffyduck 20458f792b release: bump version to 0.1.2.0 2026-05-11 19:27:33 +02:00
duffyduckandClaude Opus 4.7 e7189a992b release: 0.1.2.0 — Text-Chat fertig, Ping-Pong stabil, OpenClaw als Basis (letzte OpenClaw-Version)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:25:46 +02:00
duffyduckandClaude Opus 4.7 76cac4b4e9 docs(issue/readme): heutige Features dokumentiert — ARIA-Files, Reparatur-Buttons, Pinch-Zoom, Auto-Compact
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:20:10 +02:00
duffyduck d7612fb8f0 release: bump version to 0.1.1.9 2026-05-11 19:17:30 +02:00
duffyduckandClaude Opus 4.7 0769e7bbf1 feat(app): Pinch-Zoom + Pan im Vollbild-Modal
Neue ZoomableImage-Komponente — reine RN-Implementation mit
PanResponder + Animated, ohne extra Dependency.

- 2-Finger-Pinch: Zoom 1x..5x, Focal-Point folgt der Geste
- 1-Finger-Pan: nur aktiv wenn gezoomt, mit Bounds-Clamping
- Doppel-Tap: Toggle 1x ↔ 2.5x

Vollbild-Modal ersetzt das simple <Image> durch ZoomableImage fuer
JPEG/PNG/etc. SVGs bleiben non-zoomable (SvgUri-Limitation), Tap
schliesst sie. Plus dedicated ✕-Close-Button oben rechts da Tap-to-
Close mit PanResponder kollidiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:15:58 +02:00
duffyduckandClaude Opus 4.7 31d05a9a65 fix(diagnostic): /shared/uploads/-Bilder im History-Load auch inline rendern
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:12:59 +02:00
duffyduckandClaude Opus 4.7 337af351e6 fix(diagnostic): ARIA-Datei-Bubbles ueberleben Browser-Refresh
Beim Page-Reload laedt Diagnostic die Chat-History aus dem OpenClaw-
Session-File. file_from_aria-Events sind nur live-broadcast, nicht im
jsonl gespeichert → nach F5 waren die Anhang-Bubbles weg.

Fix: Server parsed [FILE: ...]-Marker aus assistant-messages beim
History-Load und schickt fuer existierende Files ein "aria_file"-
Message-Stueck mit allen Metadaten (Pfad, MIME, Groesse). Frontend
ruft addAriaFile mit denen, sodass die Bubbles wieder erscheinen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:08:06 +02:00
duffyduck 56d797ccb1 release: bump version to 0.1.1.8 2026-05-11 19:04:25 +02:00
duffyduckandClaude Opus 4.7 e4c8d7189d feat(ui): ARIA-Abkuerzung ausgeschrieben in App-Ueber + Diagnostic-Einstellungen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:02:30 +02:00
duffyduck 202158c839 release: bump version to 0.1.1.7 2026-05-11 18:59:57 +02:00
duffyduckandClaude Opus 4.7 6f7c6643eb fix(bridge): User informieren wenn ARIA Marker fuer nicht-existente Datei setzt
Bridge-Logs zeigten: ARIA setzt zwei Marker (aria_rave2.mid und .mp3),
hat die .mid aber nie wirklich erstellt. Bridge filterte sie silent →
Stefan sah nur eine Bubble und dachte das Marker-System ist kaputt.

Jetzt: _extract_file_markers gibt auch eine Liste der "missing"-Pfade
zurueck, und im Antworttext steht ein Hinweis-Block fuer den User
welche Files versprochen aber nicht erstellt wurden.

Plus System-Prompt geschaerft: ARIA soll vor dem Marker pruefen ob
das File wirklich existiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:41:22 +02:00
duffyduckandClaude Opus 4.7 5921008510 fix(diagnostic): [FILE: ...]-Marker-Filter ueberall (in addChat statt nur chat_final)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:33:49 +02:00
duffyduck c1f800a46d release: bump version to 0.1.1.6 2026-05-11 02:25:38 +02:00
duffyduckandClaude Opus 4.7 125168e0ed feat: Auto-Compact nach N User-Messages — verhindert E2BIG bei langer Session
E2BIG (Argument list too long) tritt auf wenn aria-core's Subprocess-
Spawn das Linux argv-Limit (~128KB-2MB) sprengt. Bei >140 Messages
samt Memory + System-Prompt + Tools laeuft das voll, ARIA antwortet
nur noch leer auf jede Anfrage.

Bridge zaehlt jetzt User-Nachrichten in send_to_core; bei COMPACT_AFTER_MESSAGES
(env, default 140) wird automatisch:
- Sessions geleert (rm sessions/*.jsonl + sessions.json = {})
- aria-core neu gestartet
- User informiert "Konversation komprimiert, letzte Nachricht nochmal"

Plus manueller "🧹 Konversation komprimieren"-Button in App-Settings
und 🧹 Compact-Button in Diagnostic-Thinking-Indicator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:24:30 +02:00
duffyduckandClaude Opus 4.7 099999f10d fix(diagnostic): aria-restart ueber Docker-Socket-API statt CLI (Container hat kein docker installiert)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:03:08 +02:00
duffyduck 4363372e25 release: bump version to 0.1.1.5 2026-05-11 01:59:58 +02:00
duffyduckandClaude Opus 4.7 168590580c feat: "ARIA hart neu starten"-Button (docker restart aria-core)
Zweiter Eskalations-Button neben dem Reparieren-Button — fuer Faelle
wo doctor --fix nicht reicht (Run alive aber haengt im Tool-Call).
Mit Confirmation-Dialog damit's nicht versehentlich gedrueckt wird.

Wege:
- App-Settings: Reparatur-Sektion, zwei Buttons (Reparieren + Hart neu)
- App-Thinking-Bubble: 🔧 + 🚨 + Abbrechen
- Diagnostic-Thinking-Indicator: 🔧 + 🚨 + Abbrechen
- Diagnostic-Server: POST /api/aria-restart → child_process exec
  `docker restart aria-core`
- Bridge: rvs aria_restart → HTTP zu Diagnostic

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:58:44 +02:00
duffyduck f178322235 release: bump version to 0.1.1.4 2026-05-11 01:48:33 +02:00
duffyduckandClaude Opus 4.7 d73c800209 feat: "ARIA reparieren"-Button in App + Diagnostic
Bei stuck OpenClaw-Runs (ARIA antwortet nicht mehr / "Antwort ohne Text"
auf jede Anfrage) kann der User jetzt selbst openclaw doctor --fix
anstossen — ohne SSH/docker exec.

Pfad:
- App-Button → rvs.send('doctor_fix') → Bridge → HTTP POST an
  Diagnostic /api/doctor-fix → dockerExec aria-core
- Diagnostic-Button → direkt HTTP POST /api/doctor-fix

Zwei Plaetze in der App: oben in der Thinking-Bubble (wenn ARIA denkt
aber haengt) und in Settings → Reparatur (immer erreichbar). In
Diagnostic neben dem Abbrechen-Button im Thinking-Indicator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:46:35 +02:00
duffyduck 6aaf4243fc release: bump version to 0.1.1.3 2026-05-10 18:47:10 +02:00
duffyduckandClaude Opus 4.7 4e5fca6567 fix(app): SVG im Vollbild via SvgUri rendern (statt Image) — preserveAspectRatio damit nicht gestreckt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:46:13 +02:00
duffyduck f6499d9514 release: bump version to 0.1.1.2 2026-05-10 18:44:05 +02:00
duffyduckandClaude Opus 4.7 abcaea8259 fix(app): SVG-Anhaenge mit SvgUri rendern statt Image
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:42:53 +02:00
duffyduckandClaude Opus 4.7 a2a9ed06df fix(app): react-native-svg auf 14.1 (kompatibel mit RN 0.73) — 15.x braucht neuere RN-Version
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:35:15 +02:00
duffyduckandClaude Opus 4.7 d7cecf2ea0 feat(app): SVG-Inline-Rendering via react-native-svg
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:33:30 +02:00
duffyduckandClaude Opus 4.7 11fe69d182 docs(prompt): ARIA soll externe Bilder/Files runterladen statt nur verlinken
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:32:16 +02:00
duffyduckandClaude Opus 4.7 3b42e59715 feat(app): Inline-Bilder in Chat-Nachrichten anzeigen (wie in Diagnostic)
MessageText erkennt http(s)-URLs auf Bilder (jpg/png/gif/webp/bmp/ico)
und rendert sie als <Image> unter dem Text. Markdown-Syntax
![alt](url) wird durch dasselbe Regex erfasst weil die URL drin ist.
SVGs ausgespart — React Native Image kann SVG nicht ohne Extra-Lib.

Aspect-Ratio wird via Image.getSize ermittelt, gecapped auf 0.5..2.5
damit Panorama-/Streifen-Bilder die Bubble nicht sprengen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:30:55 +02:00
duffyduckandClaude Opus 4.7 ae087bfd33 feat(diagnostic): ARIA-Datei-Pfad als kleiner Debug-Footer in der Bubble
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:27:17 +02:00
duffyduckandClaude Opus 4.7 7f42e71b8f fix(diagnostic): [FILE: ...]-Marker aus chat_final rausfiltern
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:26:35 +02:00
duffyduckandClaude Opus 4.7 96c3e0030a fix(prompt): File-Marker-Anweisung in BOOTSTRAP.md (echter System-Prompt) statt AGENT.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:23:22 +02:00
duffyduckandClaude Opus 4.7 66b222227b docs(agent): File-Marker-Anweisung deutlich schaerfer formuliert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:35 +02:00
duffyduckandClaude Opus 4.7 cbce4dd798 chore: init.sh — Setup-Script materialisiert *.example zu Config-Dateien
Frisch geclonte Repo / git pull nach .gitignore-Aenderungen lassen
USER.md (und andere Config-Files) fehlen — docker compose up failt
dann beim Bind-Mount. init.sh kopiert idempotent alle *.example zu
ihren Originalen wenn die noch nicht existieren.

Nach git clone und git pull empfohlen: bash init.sh

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:14:21 +02:00
duffyduckandClaude Opus 4.7 a9d8c611b8 fix(android): FileOpenerModule — kein '*/*' im Source (Kotlin-Lexer-Verwirrung)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:03:59 +02:00
duffyduckandClaude Opus 4.7 9377c12476 chore(config): USER.md aus Repo nehmen — enthielt interne Tool-Liste
USER.md hatte Stefan-spezifische Infos (Gitea-URL hackersoft.de,
OpenCRM, STARFACE, RustDesk Tool-Stack). Hat im Repo nichts zu suchen.

USER.md ist jetzt in .gitignore, USER.md.example als Vorlage
eingecheckt. Lokale USER.md bleibt funktional unangetastet — nur
nicht mehr getrackt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:00:45 +02:00
duffyduckandClaude Opus 4.7 29ffacb697 docs(agent): ARIA-System-Prompt um File-Marker-Anweisung ergaenzt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:58:36 +02:00
duffyduckandClaude Opus 4.7 5ace4ff128 feat: ARIA kann Dateien an User zurueckgeben (PDFs, Bilder, Office-Docs, ...)
ARIA setzt im Antworttext einen Marker `[FILE: /shared/uploads/aria_xxx.ext]`.
Bridge extrahiert ihn (Marker wird aus dem TTS-Text entfernt) und sendet
ein neues file_from_aria-Event ueber RVS an App + Diagnostic.

Diagnostic:
- Eigene Bubble mit Datei-Icon + Klick-Handler
- PDF/Bild → neuer Browser-Tab via /shared/* HTTP-Route
- Andere → Download via download-Attribut

App:
- Neues FileOpenerModule (Kotlin) — Intent.ACTION_VIEW mit FileProvider,
  Android-Picker waehlt App nach MIME-Type
- file_paths.xml erweitert (cache + files + external)
- file_response liefert jetzt mimeType mit
- Klick auf ARIA-Anhang: lokal vorhanden → direkt oeffnen, sonst
  file_request mit autoOpen-Flag → bei Empfang persistAttachment + open

Stefan muss noch im aria-core/OpenClaw System-Prompt einen Hinweis
einbauen: "Wenn du dem User eine Datei erstellt hast (Pfad in
/shared/uploads/), haenge am Ende deiner Antwort einmalig
[FILE: /shared/uploads/aria_<name>.<ext>] an. Der Marker wird aus dem
sichtbaren Text entfernt und als Anhang in App und Diagnostic angezeigt."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:56:47 +02:00
duffyduckandClaude Opus 4.7 24fd1b5960 docs(issue): heutige Session-Fixes ergaenzt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:32:05 +02:00
duffyduck 2c34d44fb9 release: bump version to 0.1.0.9 2026-05-10 17:25:48 +02:00
duffyduckandClaude Opus 4.7 10b75edd9a revert(audio): kickReleaseMedia raus — bricht Spotify's Auto-Resume
Logs zeigen jetzt KEINEN haengenden RNSound-Focus mehr (Library-Version
oder Sound-Lifecycle hat sich geaendert). Der Kick mit AUDIOFOCUS_GAIN
(permanent) sagte Spotify "user hat manuell etwas anderes gestartet" →
Spotify resumed nicht automatisch.

Ohne Kick: unser Focus war AUDIOFOCUS_GAIN_TRANSIENT (USAGE_ASSISTANT) —
beim release bekommt Spotify einen sauberen GAIN nach TRANSIENT-Loss
und resumed automatisch.

Native kickReleaseMedia bleibt fuer den Fall dass es nochmal gebraucht
wird, wird aber nicht mehr gerufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:24:55 +02:00
duffyduck 43f3a6b098 release: bump version to 0.1.0.8 2026-05-10 17:21:46 +02:00
duffyduckandClaude Opus 4.7 75881255d9 fix(audio): Mute-Button = Stop fuer aktuelle Antwort, nie Resume
Bisheriges Verhalten: Mute drueckt → stopPlayback. Mute zurueck → noch
eingehende chunks der gleichen Antwort starteten einen neuen Stream und
ARIA redete weiter wo sie war. Funktionierte nur 2x weil dann isFinal
schon kam und keine chunks mehr fluten.

Stefan: "Mund verbieten = Stop, fertig". Neue Antworten sollen normal
spielen.

Fix: _stoppedMessageId-Tracking. Bei Mute=true wird die aktuelle msgId
gemerkt — alle weiteren chunks dieser msgId bleiben silent, auch wenn
Mute zurueckgenommen wird. Reset bei neuer msgId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:20:29 +02:00
duffyduck 5f4c8184de release: bump version to 0.1.0.7 2026-05-10 17:14:11 +02:00
duffyduckandClaude Opus 4.7 46bf37a05e fix(audio): kickReleaseMedia auch im PCM-Pfad — re-renderte Antworten muteten Spotify dauerhaft
Stefan: ältere Nachrichten (deren Cache-WAV weg ist) gehen ueber
tts_request neu rendern → kommen als PCM-Stream zurueck → werden ueber
PcmStreamPlayer abgespielt. Beim Mute lief stopPlayback aber ohne den
Spotify-resume-Kick weil hadRnSound=false war (kein currentSound).

Jetzt: kickReleaseMedia immer in stopPlayback rufen — kostet nichts,
deckt PCM- und RNSound-Pfad ab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:13:12 +02:00
duffyduck d3bf0654f0 release: bump version to 0.1.0.6 2026-05-10 17:07:53 +02:00
duffyduckandClaude Opus 4.7 8ae3d83082 fix(audio): kickReleaseMedia mit 250ms Pause zwischen request+abandon — Spotify kriegt den Focus-Wechsel sonst gar nicht mit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:07:03 +02:00
duffyduck 3fa4439441 release: bump version to 0.1.0.5 2026-05-10 17:02:56 +02:00
duffyduckandClaude Opus 4.7 db72674a50 fix(audio): stopPlayback idempotent — kein doppelter Focus-Kick
Re-Renders / setInterval(loadSettings) triggern setMuted(true) oft
mehrfach hintereinander → jeder weitere stopPlayback rief erneut
kickReleaseMedia, Spotify pausierte+resumte mehrfach (Stefan: "spielt
kurz und pausiert dann wieder").

Fix: stopPlayback returnt sofort wenn nichts mehr aktiv ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:02:01 +02:00
duffyduck d425e1a759 release: bump version to 0.1.0.4 2026-05-10 16:59:15 +02:00
duffyduckandClaude Opus 4.7 dbd7ffd534 fix(audio): Spotify resumed nach Mute — RNSound's haengenden Focus loesen
Logs zeigten: react-native-sound requestet beim Sound.play() einen
EIGENEN AudioFocus mit USAGE_MEDIA, released den aber bei Sound.stop()/
release() NICHT (bekanntes RN-sound-Bug). Spotify sieht den haengenden
Media-Focus → bleibt pausiert.

Workaround: Native-Methode kickReleaseMedia() macht einen request+abandon-
Cycle mit USAGE_MEDIA, das System raeumt damit den Focus-Stack auf und
Spotify bekommt sauberen GAIN-Event. stopPlayback ruft das jetzt nach
Sound.release() wenn vorher ein RNSound aktiv war.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:57:52 +02:00
duffyduckandClaude Opus 4.7 98f4327784 feat(logger): Verbose-Logging-Toggle in Settings → Protokoll
console.log wird global stummgeschaltet wenn aus — spart adb-logcat-
Speicher wenn alles laeuft. console.warn/error bleiben immer aktiv.
Default an.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:52:25 +02:00
duffyduck abc8a666b6 release: bump version to 0.1.0.3 2026-05-10 16:43:37 +02:00
duffyduckandClaude Opus 4.7 990e7bb0e4 fix(chat): Play-Button rendert neu wenn Cache-Datei weg
Vorher: Button checkte nur ob audioPath gesetzt ist — auf eine geloeschte
Cache-Datei hat aber nichts geprueft. playFromPath warntete nur und
returnte stumm. Jetzt wird VOR playFromPath die Existenz geprueft, sonst
geht's ueber tts_request an die Bridge zum Neu-Rendern.

Plus: Logs in Sound.play-Callback und _releaseFocusDeferred fuer den
"Spotify resumed nicht nach Replay"-Bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:42:38 +02:00
duffyduck 8c4fa7a49f release: bump version to 0.1.0.2 2026-05-10 16:38:00 +02:00
duffyduckandClaude Opus 4.7 b2107591aa feat(audio): Cache-Cleanup beim App-Start + TTS-Cache-Settings-Button
- App-Start raeumt orphane aria_tts_*.wav (>5min) aus dem Cache —
  Wiedergaben die durch Anruf/Mute/Barge-In abgebrochen wurden
  hinterliessen sonst Files, weil der completion-Callback nicht feuert.
- Neuer Settings-Button "TTS-Cache leeren" mit Live-Groessenanzeige —
  parallel zum bestehenden "Update-Cache leeren".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:36:01 +02:00
duffyduck 7b3a4e8655 release: bump version to 0.1.0.1 2026-05-10 16:30:14 +02:00
duffyduckandClaude Opus 4.7 b6a82d7701 fix(audio): pauseForCall setzt isPlaying zurueck — Playback nach Anruf nicht mehr tot
pauseForCall stoppte zwar currentSound + setzte ihn auf null, hat aber
isPlaying=true gelassen. Folge: nach dem Anruf war jeder weitere Play-
Button-Klick wirkungslos, weil playAudio bei isPlaying=true den
_playNext-Pfad ueberspringt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:28:42 +02:00
duffyduck ec5843c54f release: bump version to 0.1.0.0 2026-05-10 16:22:08 +02:00
duffyduckandClaude Opus 4.7 f5435d9367 fix(phone): 800ms-Delay vor Auto-Resume — Spotify kommt zum Atmen
Wenn ARIA's Resume-Pfad direkt nach Anruf-Ende den AudioFocus requestet,
kollidiert das mit Spotify's eigenem Auto-Resume. System haengt noch im
IN_CALL-Mode-Uebergang, Spotify sieht "Loss → Loss" und bleibt pausiert
statt kurz zu resumen.

Mit 800ms-Delay: Spotify schafft den Resume-Schritt, dann pausiert ARIA
wieder ordnungsgemaess. Wenn ARIA nichts pending hatte, bleibt Spotify
einfach weiter an.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:20:48 +02:00
duffyduckandClaude Opus 4.7 1c28d88a35 feat(bridge): Bilder >2MB serverseitig auf 1568px verkleinern
Claude-Vision-API hat ~5MB Base64-Limit. Stefan's 4MB Foto via
Buroklammer (DocumentPicker) sprengte das, Claude lieferte leere
Antwort zurueck. Galerie-Pfad ging weil react-native-image-picker
schon clientseitig komprimiert.

Bridge resized jetzt JPEG/PNG/WebP/GIF >2MB auf max 1568px lange
Seite (Anthropic-Empfehlung), JPEG q=85. SVG, PDF, ZIP, Office-Docs
bleiben unangetastet — die laufen ueber Tools, nicht Vision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:15:05 +02:00
duffyduckandClaude Opus 4.7 a22d8e587f fix(bridge): "chat final ohne Text" wird sichtbar an App gemeldet
Wenn Claude-Vision das Bild silent ablehnt (z.B. zu gross), kommt
phase=end ohne Crash, aber chat:final ohne text. Bridge ignorierte das
nur mit Warning — App wartete ewig auf Antwort. Jetzt kommt eine
Hinweis-Bubble damit der User weiss dass was schief lief.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:11:14 +02:00
duffyduckandClaude Opus 4.7 35a9d12497 fix(bridge): chat:error liest auch errorMessage — kein "Unbekannt" mehr
OpenClaw legt bei state=error den Text in errorMessage statt error.
Bridge ignorierte das und meldete generisches "[Fehler] Unbekannt" an
App + Diagnostic — der echte Text ("Process exited with code 1" etc)
ging nur in die Container-Logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:07:20 +02:00
duffyduck 437959bd69 release: bump version to 0.0.9.9 2026-05-10 15:56:53 +02:00
duffyduckandClaude Opus 4.7 401617ffff debug+fix(audio): Mute-Logs + resumeSound auch in stopPlayback stoppen
stopPlayback stoppte bisher nur currentSound, nicht resumeSound — wenn
nach einem Anruf der Auto-Resume laeuft und der User Mute drueckt, bleibt
der Resume-Sound weiter spielen.

Plus Logs in setMuted/stopPlayback um zu sehen warum Stefans Mute beim
Replay nicht greift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:55:51 +02:00
duffyduck f93b769048 release: bump version to 0.0.9.8 2026-05-10 15:46:36 +02:00
duffyduckandClaude Opus 4.7 f48c7a05a9 fix(audio): _firePlaybackStarted ueberschrieb playFromPath-Tracking mit leerem pcmMessageId
Logs zeigten: playFromPath setzt currentPlaybackMsgId='db710ff3-...', 9s
spaeter beim Anruf war captureInterruption msgId=(leer). Ursache:
_firePlaybackStarted setzt currentPlaybackMsgId blind aus pcmMessageId —
das ist beim Play-Button leer.

Jetzt nur noch setzen wenn ein PCM-Stream laeuft. Play-Button und Resume-
Sound setzen ihr Tracking selber im Caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:44:28 +02:00
duffyduckandClaude Opus 4.7 6216c58bc0 fix(bridge): WebSocket max_size auf 50MB — grosse Bilder/Uploads gehen wieder
Python websockets Default-Limit ist nur 1 MiB. Stefan's 4MB JPEG (5.8MB als
Base64) sprengte das, Bridge-Verbindung wurde silent gedroppt. App sah
nichts, Diagnostic kriegte kein file_saved, ARIA reagierte nicht — Kamera-
Bilder waren klein genug (<1MB) und gingen darum durch.

f5tts/whisper-bridges hatten max_size=50MB schon drin, nur aria_bridge
hatte's an beiden websockets.connect-Stellen vergessen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:42:48 +02:00
duffyduck 056e87241d release: bump version to 0.0.9.7 2026-05-10 15:37:45 +02:00
duffyduckandClaude Opus 4.7 fd8145054b fix(audio): msgId-Regex liberaler — auch nicht-UUID-Dateinamen werden erkannt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:36:43 +02:00
duffyduck f5b78b85bb release: bump version to 0.0.9.6 2026-05-10 15:29:03 +02:00
duffyduckandClaude Opus 4.7 48abcda873 debug(chat): Logs fuer Anhang-Send-Pipeline
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:27:24 +02:00
duffyduckandClaude Opus 4.7 addc6d3069 fix(audio): Play-Button setzt jetzt auch Wiedergabe-Tracking — Anruf-Test via Playback funktioniert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:22:21 +02:00
duffyduckandClaude Opus 4.7 a9adb6562c debug+fix(audio): mehr Anruf-Logs + Tracking auch beim Resume-Sound
Im Test 2 (zweiter Anruf in derselben Antwort) kam weder captureInterruption
noch resumeFromInterruption als Log — beide returnen frueh ohne Hinweis warum.
Jetzt loggen sie auch den Skip-Pfad damit man sieht ob's der idempotent-Guard
oder fehlende playbackStartTime ist.

Plus: _playFromPathAtPosition aktualisiert jetzt currentPlaybackMsgId und
playbackStartTime — sonst stehen die auf den Werten der ersten TTS-Wiedergabe
und ein zweiter Anruf-captureInterruption wuerde mit veraltetem Stand laufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:20:43 +02:00
duffyduck 15beee0fa6 release: bump version to 0.0.9.5 2026-05-10 15:13:38 +02:00
duffyduckandClaude Opus 4.7 835eea0706 fix(audio): Auto-Resume nach Anruf — pcmBuffer bleibt erhalten
Logs zeigten "WAV nicht binnen 30000ms verfuegbar" — pcmBuffer wurde von
haltAllPlayback geleert, isFinal schrieb daher eine leere WAV (oder gar
keine, weil pcmMessageId leer war).

Neue Methode pauseForCall (statt haltAllPlayback im Anruf-Pfad):
- AudioTrack stoppt + AudioFocus release (Spotify resumed)
- pcmBuffer + pcmMessageId BLEIBEN — Bridge-Chunks werden weiter gesammelt
- _pausedForCall macht weitere Chunks "silent" (kein writeChunk, nur Cache)
- isFinal schreibt WAV trotz Anruf → resumeFromInterruption findet sie

Plus captureInterruption idempotent gemacht: ringing→offhook ueberschreibt
die Position vom ersten Halt nicht mehr (Date.now-Tracking laeuft stumpf
weiter obwohl Audio gestoppt ist — der erste Halt ist die echte Position).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:11:46 +02:00
duffyduckandClaude Opus 4.7 4ae8063797 docs(issue): vier neue Fixes der Debug-Session festgehalten
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:01:37 +02:00
duffyduckandClaude Opus 4.7 a9f1134b0b fix(bridge): GPS-Position auch im STT-chat-Payload an Diagnostic mitgeben
Die App sendet location einmal im audio-Payload. Die Bridge kannte sie
zwar (ging in aria-core's Kontext rein), reichte sie aber nicht im STT-
broadcast an die Diagnostic durch. Diagnostic zeigte darum bei Sprach-
eingaben nie den GPS-Block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:59:09 +02:00
duffyduck 902362ca24 release: bump version to 0.0.9.4 2026-05-10 14:54:45 +02:00
duffyduckandClaude Opus 4.7 cc90ff4131 debug(gps): Logs fuer Standort-Abfrage und Permission-Fehler
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:53:32 +02:00
duffyduck 8c27090ef1 release: bump version to 0.0.9.3 2026-05-10 14:48:35 +02:00
duffyduckandClaude Opus 4.7 4afae31e56 fix(gps): Standort-Permission anfordern — sonst sendet App nie eine Position
Im Manifest fehlte ACCESS_COARSE/FINE_LOCATION komplett, und der
Settings-Toggle requestete keine Runtime-Permission. Geolocation
.getCurrentPosition() schlug darum lautlos fehl, App sendete nie ein
location-Feld → Diagnostic konnte nichts anzeigen, auch wenn der
Diagnostic-eigene "GPS einblenden"-Toggle aktiv war.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:47:35 +02:00
duffyduckandClaude Opus 4.7 faf9c0098b fix(audio): AudioTrack-Start-Threshold auf 100ms — kurze TTS startet jetzt
ENDLICH die Wurzel: AudioTrack hat seit API 31 setStartThresholdInFrames(),
default ist bufferSize/2. Bei 4s-Buffer = 2s Threshold — Track wartet bis
2s im Buffer sind, sonst startet play() nie wirklich (pos bleibt 0).

Bei 3 Worten (~1.4s) kommt's nie ueber die Schwelle. Threshold runter
auf 100ms (2400 Frames @ 24kHz) — Track laeuft sofort mit erstem Chunk an.

Erklaert auch warum genau ab 9 Worten (~3s+) der Pre-Roll-Pfad lief: dann
wurde die 2s-Schwelle ueberschritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:45:05 +02:00
duffyduck e944d35263 release: bump version to 0.0.9.2 2026-05-10 14:40:35 +02:00
duffyduckandClaude Opus 4.7 03faf9ce05 fix(audio): Mute-Button stoppt jetzt auch laufenden PCM-Stream
pcmStreamActive wurde beim isFinal-Chunk schon auf false gesetzt, der
AudioTrack spielte aber noch aus seinem Buffer (kann sekundenlang sein).
stopPlayback() uebersprang darum PcmStreamPlayer.stop() — ARIA redete
weiter obwohl Spotify schon resumed war.

Fix: stop() immer rufen, der Flag-Check faellt weg (ist eh idempotent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:39:27 +02:00
duffyduckandClaude Opus 4.7 7187c2bf3f fix(audio): play() beim 1. Chunk — kurze Texte stallen nicht mehr
Logs zeigten: Pre-Roll-Pfad (play() WAEHREND chunks reinkommen) lief
immer sauber, Kurz-Text-Pfad (play() NACHDEM Buffer komplett gefuellt
ist) stallte immer — egal mit wie viel Daten oder welchem USAGE-Tag.

Fix: play() beim allerersten data-chunk callen, kein Pre-Roll-Threshold
mehr. AudioTrack ist sofort im PLAYING-State, weitere chunks/trailing
fliessen parallel ab. Padding-Block nach mainLoop entfaellt komplett.

USAGE_MEDIA wieder auf USAGE_ASSISTANT zurueck — war nicht die Ursache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:38:02 +02:00
duffyduck e717c0f98e release: bump version to 0.0.9.1 2026-05-10 14:32:35 +02:00
duffyduckandClaude Opus 4.7 1f2aeb8d88 fix(audio): AudioTrack auf USAGE_MEDIA — USAGE_ASSISTANT stallt auf OnePlus A12
Letzter Test zeigte: 163456B im Buffer mit play()-nach-Padding stallt
(pos=0), aber 170048B im Pre-Roll-Pfad startet einwandfrei. Differenz
nur 4% Daten — kein Buffer-Threshold-Problem, sondern AudioTrack-Quirk
mit USAGE_ASSISTANT bei "voller Buffer, dann play()".

USAGE_MEDIA ist robuster, AudioFocus laeuft eh separat ueber das
AudioFocusModule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:31:23 +02:00
duffyduck 15106ca011 release: bump version to 0.0.9.0 2026-05-10 14:26:41 +02:00
duffyduckandClaude Opus 4.7 55ce0372ac fix(audio): kurze TTS — Padding auf 3s erhoeht (OnePlus A12 Hard-Threshold)
Test mit 96000B (2s) Padding zeigte: AudioTrack stallt immer noch mit
pos=0/48000. Ab 8 Worten (~2.5s) geht's — der Hard-Threshold liegt also
zwischen 2s und 3s. Padding auf 3s, Buffer auf 4s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:25:43 +02:00
duffyduck a2aff77377 release: bump version to 0.0.8.9 2026-05-10 14:20:43 +02:00
duffyduckandClaude Opus 4.7 db250ce8ba fix(audio): kurze TTS-Texte — play() erst NACH Buffer-Fuellung mit Padding
Auf OnePlus A12 startet AudioTrack nicht zuverlaessig wenn play() bei
duennem Buffer gerufen wird (pos blieb 0/34112 trotz 71KB Daten + Retry).

Neue Reihenfolge bei kurzem Stream:
1. Daten in Buffer schreiben (mainLoop)
2. Trailing-Silence (0.3s)
3. Padding bis min. 2s gepuffert
4. DANN erst play()

Buffer auf 3s erhoeht damit blockingem write() noch Headroom bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:19:45 +02:00
duffyduck 77b48a70b2 release: bump version to 0.0.8.8 2026-05-10 14:12:59 +02:00
duffyduckandClaude Opus 4.7 278812c8d7 fix(audio): kurze TTS-Texte spielen wieder ab — AudioTrack-Buffer entkoppelt von Preroll
OnePlus A12 stallte bei kurzem Text mit pos=0/34112: 336KB Buffer fuer
3.5s Preroll, aber nur 68KB Daten drin → AudioTrack faehrt nicht an.

Fix: Buffer fest auf ~2s, plus play()-Retry bei pos=0 nach 500ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:11:53 +02:00
duffyduck e1ca01f507 release: bump version to 0.0.8.7 2026-05-10 14:00:29 +02:00
duffyduckandClaude Opus 4.7 af5b203b17 fix(phone-call): kein VoIP-Toast bei Play-Button — AudioMode pruefen
Stefan: 'Möchte ich mir playbacks anhören egal welches kommt die toast
nachricht voip anruf und danach aria wieder aktiv'.

Ursache: AUDIOFOCUS_LOSS feuert bei jedem Audio-Player-Wechsel
(Spotify, andere Apps, sogar unsere eigenen Sound-Calls). Wir
interpretierten das blind als VoIP-Anruf.

Fix: vor dem Halt fragen wir AudioFocus.getMode() ab — nur wenn
mode == 2 (IN_CALL) oder 3 (IN_COMMUNICATION) ist's wirklich ein
Anruf. Bei NORMAL (0) wird der Loss ignoriert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:43:40 +02:00
duffyduck 27a43ad0e8 release: bump version to 0.0.8.6 2026-05-10 12:50:35 +02:00
duffyduckandClaude Opus 4.7 0ade37d3be feat(update): APK-Cache robuster + manueller 'Update-Cache leeren' Button
Stefan: 'app blaeht sich auf durch heruntergeladene Update-Versionen'.

updater.ts:
- cleanupOldApks durchsucht jetzt 4 Pfade (Caches, Documents, ExternalCaches,
  ExternalDir) statt nur CachesDirectoryPath
- Public gemacht + returnt {removed, freedMB}
- getApkCacheSize() neu — listet count + totalMB

SettingsScreen → Speicher:
- Neue Sektion 'Update-Cache' mit Live-Groessenanzeige
- Button 'Update-Cache leeren' triggert cleanup + Toast mit Ergebnis
- Beim Mount wird die Groesse einmal geladen

Auto-Cleanup laeuft weiterhin beim App-Start + vor jedem Download —
der Button ist fuer den Notfall (haengender Download, alte Pfade,
defekte APKs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:49:22 +02:00
duffyduckandClaude Opus 4.7 122b4d05df docs: Auto-Resume + Edge-Cases in issue.md + README
issue.md: Audio-Tabelle erweitert um 'neue Frage waehrend Anruf' und
'Anruf vorbei nach neuer Frage'. Mechanismen-Liste ergaenzt mit
'Audio-Ausgabe waehrend Telefonat' (state-change Logik) und 'neue
Frage verwirft pending Resume'. Drei neue Erledigt-Eintraege fuer
VoIP, Auto-Resume und PcmPlaybackFinished-Event.

README: kompakte Audio-Tabelle ergaenzt + Roadmap zwei neue Bullets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:41:34 +02:00
duffyduckandClaude Opus 4.7 a342aa19a1 fix(audio): neue ARIA-Antwort verwirft pending Auto-Resume
Stefans Edge-Case: waehrend des Telefonats stellt der User eine neue
Text-Frage. Die neue ARIA-Antwort startet sofort (offhook→offhook
loest keinen halt aus). Vorher haette resumeFromInterruption nach
Anruf-Ende noch die ALTE Antwort (die unterbrochen wurde) ab
Position spielen wollen — Konflikt mit der neuen Antwort.

Fix: in _handlePcmChunkImpl beim Wechsel zu einer neuen messageId:
- laufenden resumeSound stoppen
- pausedMessageId = '' wenn != neue messageId

Damit gewinnt immer die neueste Antwort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:39:33 +02:00
duffyduckandClaude Opus 4.7 0f72cea2b5 feat(audio): Auto-Resume nach Anruf ab der gemerkten Position
Stefans Idee: Position beim Halt merken (Date.now() - playbackStart -
leadingSilence), nach dem Auflegen ab da weitermachen. Wenn der Cache
noch nicht komplett ist (final-Marker kam waehrend Anruf), warten wir
bis zu 30s auf das WAV — meistens ist's schon da weil das Telefonat
laenger als die Antwort dauerte.

audio.ts:
- captureInterruption(): merkt position + messageId, returnt Sekunden
- resumeFromInterruption(maxWaitMs): wartet auf WAV-Cache, lädt mit
  Sound, setCurrentTime(position), play
- Tracking-Felder: playbackStartTime, currentPlaybackMsgId, pausedX

phoneCall.ts:
- _haltForCall ruft captureInterruption() VOR haltAllPlayback
- _resumeAfterCall triggert resumeFromInterruption(30s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:37:35 +02:00
duffyduckandClaude Opus 4.7 e4781fd353 fix(audio): AudioFocus erst beim NATIVEN Playback-Finished-Event released
Logcat-Befund:
12:22:54.860 — final-Chunk + Cache geschrieben
12:22:55.402 — abandonAudioFocus (~0.5s spaeter)
12:22:55     — Spotify resumed (Atlas: TotalTime 93s)
12:23:27.064 — Playback fertig (32s spaeter!)

→ ARIA spricht 32s parallel zu Spotify weil end() viel zu frueh
returnt. Stefans 'Spotify resumed obwohl ARIA noch redet'.

Fix:
- PcmStreamPlayerModule emittiert 'PcmPlaybackFinished' RN-Event nach
  dem finally{}-Block im Writer-Thread (= AudioTrack hat alle Samples
  wirklich durchgespielt)
- audioService subscribed im constructor → ruft erst dann
  _releaseFocusDeferred()
- _handlePcmChunkImpl bei isFinal triggert NICHT mehr direkt das
  Release — nur die playbackFinished-Listener (UI-Logic)

So bleibt Spotify pausiert bis ARIA tatsaechlich fertig ist, egal
wie viel Audio im AudioTrack-Buffer wartet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:29:55 +02:00
duffyduckandClaude Opus 4.7 67d0cf00a4 feat(diagnostic): GPS-Position als Debug-Block in Chat einblendbar
Toggle 'GPS-Position einblenden' rechts neben 'TTS-Text einblenden'.
Wenn aktiv und ein chat-Event hat ein location-Feld, erscheint unter
der Bubble ein gruener Block mit lat/lon — Klick oeffnet OpenStreetMap
am Punkt.

Nur Diagnostic, keine Anzeige in der App. Der Block taucht nur bei
User-/STT-/Diagnostic-Nachrichten auf (sender != aria), weil aria-core
sich nicht selbst lokalisiert.

Toggle-State wird in localStorage persistiert (aria-show-gps-debug).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:10:34 +02:00
duffyduck 78b134933c release: bump version to 0.0.8.5 2026-05-10 12:01:46 +02:00
duffyduckandClaude Opus 4.7 17a6c46fd6 docs: Audio-Verhaltens-Tabelle in issue.md + README
Definiert klar wann Spotify pausiert und wann nicht — als Referenz
fuer kuenftige Bug-Reports. Aktueller Zustand nach den Audio-Fixes:
Spotify pausiert nur waehrend User-Aufnahme + TTS-Wiedergabe, nicht
waehrend ARIAs Denkphase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:00:41 +02:00
duffyduckandClaude Opus 4.7 ed176f1276 fix(audio): rollback agentActivity-Conversation-Focus, Spotify pausiert NUR bei TTS
Der vorige Commit (acquireConversationFocus bei agentActivity != idle) war zu
aggressiv — Spotify pausierte schon waehrend 'ARIA denkt/schreibt' und das
zugehoerige release greift nicht zuverlaessig (Race mit nachfolgenden
agent_activity-Events). Stefan: 'spotify resumet nicht mehr, hoert schon
beim ARIA-denkt-Passus auf zu spielen'.

Erwartetes Verhalten:
- Aufnahme: AudioFocus → Spotify pausiert (~5s)
- ARIA denkt/schreibt (~20s): kein Focus → Spotify spielt weiter
- TTS: AudioFocus per requestDuck → Spotify pausiert
- TTS-Ende: deferred release nach 800ms → Spotify resumed

Underrun-Schutz im PcmStreamPlayer haelt Spotify durchgehend gepaust
solange TTS rendert (auch in den GPU-Pausen zwischen Saetzen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:59:13 +02:00
duffyduck 0817d775a3 release: bump version to 0.0.8.4 2026-05-10 11:53:48 +02:00
duffyduckandClaude Opus 4.7 38390c7a6a fix(audio): Spotify spielt nicht mehr in der ARIA-Verarbeitungspause
Logcat-Befund: zwischen User-Aufnahme-Ende und TTS-Start liegt eine
~20s-Pause (Whisper STT + Claude + F5-TTS). In dieser Zeit hatte ARIA
keinen AudioFocus → Spotify lief munter weiter, dann pausierte beim
TTS-Start. Stefan hoerte das als 'Spotify kommt nach 20s wieder'.

Fix: ChatScreen ruft acquireConversationFocus sobald ein agent_activity-
Event mit activity != 'idle' kommt. Solange ARIA arbeitet (thinking/
tool/responding) bleibt der Focus gehalten, Spotify bleibt pausiert.
Bei onPlaybackFinished oder cancelRequest wird releaseConversationFocus
gerufen — sonst bliebe Spotify ewig stumm.

Funktioniert auch fuer reine Text-Chats (kein Wake-Word noetig).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:52:07 +02:00
duffyduck d62f3e6d91 release: bump version to 0.0.8.3 2026-05-10 11:46:26 +02:00
duffyduckandClaude Opus 4.7 999b897e05 fix(tts): Idle-Cutoff im PCM-Writer von 30s auf 120s
Bug-Vermutung: lange F5-TTS-Antworten reissen ab wenn die Gamebox
zwischen Saetzen >30s braucht (Modell-Wechsel, kalte GPU, ungewoehnlich
schwerer Satz). Writer-Thread brach dann mit 'Idle-Cutoff' ab und
ARIA verstummte mitten im Text.

120s deckt auch lange GPU-Pausen ab. Bei echtem Bridge-Crash brauchen
wir trotzdem irgendwann einen Cutoff damit der Foreground-Service
nicht ewig haengt.

Stefan kann ADB-Logs gerade nicht ziehen (telefoniert) — bei Bug 3
(Spotify) muessen wir noch die Native-Logs sehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:37:59 +02:00
duffyduckandClaude Opus 4.7 1ed97d2f91 fix: dB-Range -85, Mute haert auch laufende TTS, VoIP-Anrufe + Bild-Bubble
Bug 1 — dB-Range erweitert:
VAD_SILENCE_DB_MIN von -55 auf -85 dB. Damit hat Stefan einen weiten
Regler-Spielraum wenn die adaptive Auto-Erkennung in seiner Umgebung
nicht zuverlaessig greift.

Bug 5 — Mute-Button stoppt laufende TTS nicht:
audioService bekommt jetzt einen internen _muted-Flag. handlePcmChunk
setzt silent automatisch wenn _muted true ist, playAudio kehrt frueh
zurueck. Verhindert Race zwischen User-Klick auf Mute und einem
TTS-Chunk der im selben JS-Tick ankommt (vorher: Ref-Update via
useEffect erst nach dem Re-Render → Chunks "rutschten durch"). Plus
ttsCanPlayRef wird im toggleMute-Handler synchron aktualisiert.

Bug 4 — VoIP/Messenger-Anrufe erkennen:
AudioFocusModule emittiert jetzt "AudioFocusChanged" Events mit type
"loss"/"loss_transient"/"gain". WhatsApp/Signal/Discord/etc. requestn
AudioFocus_GAIN_TRANSIENT_EXCLUSIVE wenn ein Anruf reinkommt — wir
fangen das in phoneCall.ts ab und rufen halt + pauseForCall genau
wie beim klassischen Anruf. Plus getMode() Polling-Fallback (alle 3s)
weil GAIN nicht zuverlaessig kommt wenn wir den Focus selbst released
haben — sobald AudioMode wieder NORMAL ist, resumeFromCall.

Bug 6 — Bilder als "Strich":
attachmentImage hatte width: '100%' in einer Bubble mit maxWidth: '80%'
ohne explizite Parent-Breite → RN rendert auf 0px Breite. Neue ChatImage-
Komponente nutzt Image.getSize um die echte aspectRatio zu messen + setzt
sie dynamisch. Bubble passt sich dem Bild an.

Bugs 2 (lange Texte mid-cutoff) + 3 (Spotify resumed) — brauchen ADB-Logs.
ADB-WLAN ueber 192.168.0.22:5555 schlaegt fehl (refused) — bei Android
11+ braucht's Wireless-Debugging-Pairing-Code. Stefan kann den nennen
sobald er soweit ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:28:52 +02:00
duffyduck 732cfda799 release: bump version to 0.0.8.2 2026-05-07 08:31:06 +02:00
duffyduckandClaude Opus 4.7 47c44e57f3 feat(vad): Stille-Pegel manuell in Settings + Info-Modal
Wenn die adaptive Baseline-Logik in einer Umgebung nicht zuverlaessig
greift (Stefan: "manchmal funktioniert die Stille-Erkennung nicht"),
kann der User die Schwelle jetzt manuell setzen.

Settings → Spracheingabe:
- "Stille-Pegel (dB)" mit −1/+1 Buttons + "Auf automatisch zuruecksetzen"
- Range −55 bis −15 dB, default "auto" (= adaptive Baseline)
- Info-Icon (i) oeffnet Modal mit Erklaerung:
  • dB-Skala (negativ, naeher 0 = lauter)
  • Faustregel-Pegel mit Farb-Code (−45 sensibel, −38 ausgewogen, −25 robust)
  • Klarstellung "niedrigere Zahl = sensibler"

audio.ts:
- VAD_SILENCE_DB_OVERRIDE_KEY in AsyncStorage
- loadVadSilenceDbOverride() liefert null oder Zahl
- startRecording: wenn Override gesetzt, Adaptive-Baseline uebersteuert.
  Speech-Schwelle wird auf Override + 10 dB gesetzt. Toast zeigt
  "VAD: manuell stille>-XX dB"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 08:24:26 +02:00
duffyduckandClaude Opus 4.7 63c61ba3f6 fix(audio): Wake-Word-Anruf-Pause + Resume-Cooldown + Background-Mic-Order
Bug 4 — Wake-Word laeuft bei Anruf weiter:
phoneCall ruft jetzt wakeWordService.pauseForCall bei RINGING/OFFHOOK
und resumeFromCall bei IDLE. Telefonie-App belegt das Mikro waehrend
des Anrufs, openWakeWord muss daher pausieren. Pre-Call-State wird
gemerkt — armed bleibt armed, conversing degraded zu armed (sonst
landet der User nach Auflegen in einem halben Dialog).

Bug 3 — App-Resume triggert faelschlich Wake-Word:
Beim Wechsel von Background nach Foreground gibt's Audio-Pegel-Spikes
(AudioFocus-Switch, AudioTrack re-route), die openWakeWord als Wake-
Word interpretiert. Neuer Cooldown-Mechanismus: AppState-Listener im
ChatScreen ruft wakeWordService.setResumeCooldown(1500) — Detections
in der Phase werden in onWakeDetected verworfen.

Bug 1 — Background-Aufnahme klappt nicht:
acquireBackgroundAudio('rec') wird jetzt VOR audioService.startRecorder
gerufen, acquireBackgroundAudio('wake') VOR OpenWakeWord.start. Sonst
greifen Androids Background-Mic-Restrictions (ab 11+) — der Service mit
foregroundServiceType=microphone muss zum Zeitpunkt des AudioRecord-
Starts schon aktiv sein, nicht erst per state-change-Listener
asynchron danach.

Bug 2 (VAD manchmal nicht): nicht in diesem Commit, vermutlich
umgebungsabhaengig. Toast zeigt die kalibrierten Schwellen — wenn
das nochmal auftritt, schick mir die Werte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:49:02 +02:00
duffyduck 455eae2cfa release: bump version to 0.0.8.1 2026-05-06 23:49:36 +02:00
duffyduckandClaude Opus 4.7 bb0e48854a chore: Highlight-Trigger raus + letzte Piper-Reste aufgeraeumt
Highlight-Trigger:
- diagnostic/index.html: Settings-Sektion + Trigger-Liste-Handler raus
- diagnostic/server.js: get_triggers / save_triggers Action-Handler +
  TRIGGERS_FILE Konstante + handleGetTriggers/handleSaveTriggers Funktionen weg
- README.md: highlight_triggers.json aus dem Datenverzeichnis-Diagram entfernt

Die Auswertung war seit Piper-Removal eh tot — die Datei wurde nur noch
geschrieben aber nirgends gelesen.

Piper-Reste:
- bridge/aria_bridge.py: Modul-Docstring auf F5-TTS aktualisiert,
  Ramona/Thorsten-Erwaehnungen raus, Inline-Kommentar zu "Komponenten
  TTS" gefixt
- aria-data/config/AGENT.md: Stimmen-Tabelle (Ramona/Thorsten) durch
  Hinweis auf F5-TTS Voice-Cloning ersetzt
- aria-data/config/BOOTSTRAP.md: gleiche Tabelle weg, Bridge-Beschreibung
  auf "orchestriert STT/TTS via Gamebox-Bridges" geaendert

Erledigt-Eintraege in issue.md + README markiert (historisch erhalten).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:47:55 +02:00
duffyduckandClaude Opus 4.7 1a4ef9e4ca feat(audio): Background-Service auch fuer Wake-Word + Aufnahme + Doku-Split
Erweitert den Foreground-Service um den microphone-Type damit nicht nur
TTS, sondern auch Wake-Word-Lauschen und aktive Aufnahmen weiterlaufen
wenn die App im Hintergrund ist.

Slot-System (backgroundAudio.ts):
- 'tts'  : ARIA spricht
- 'rec'  : Aufnahme laeuft
- 'wake' : Wake-Word lauscht passiv (Ohr aktiv)
Mehrere Slots koennen unabhaengig acquired/released werden, der Service
laeuft solange mindestens einer aktiv ist. Notification-Text passt sich
dynamisch an den hoechstprioren Slot an (tts > rec > wake).

Wiring (ChatScreen):
- onPlaybackStarted/Finished → 'tts' Slot
- audioService.onStateChange (recording) → 'rec' Slot
- wakeWordService.onStateChange (off→armed/conversing) → 'wake' Slot

AndroidManifest:
- foregroundServiceType="mediaPlayback|microphone" (Pflicht ab Android 14
  fuer Background-Mic-Zugriff)
- FOREGROUND_SERVICE_MICROPHONE Permission

Doku:
- issue.md Erledigt-Sektion in "Bugs / Fixes", "App Features" und
  "Infrastruktur" gesplittet
- README: Background-Service-Beschreibung erweitert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:43:24 +02:00
duffyduckandClaude Opus 4.7 28206476be feat(audio): Foreground-Service haelt TTS am Leben bei minimierter App
ARIAs Antwort wird jetzt auch dann fertig vorgelesen wenn der User die
App im Hintergrund schickt. Vorher hat Android den Prozess kurz nach
dem Minimieren eingefroren — TTS verstummte mitten im Satz.

Native:
- AriaPlaybackService.kt: Service mit foregroundServiceType=mediaPlayback,
  zeigt persistente Notification "ARIA spricht — antippen oeffnet die App"
  (channel low-priority, ongoing, tap → MainActivity)
- BackgroundAudioModule.kt: RN-Bridge mit start()/stop()
- AndroidManifest: FOREGROUND_SERVICE + FOREGROUND_SERVICE_MEDIA_PLAYBACK
  + POST_NOTIFICATIONS Permissions, Service deklariert

JS:
- backgroundAudio.ts: idempotenter Wrapper (active-Flag verhindert
  doppelte start/stop calls)
- ChatScreen onPlaybackStarted → startBackgroundAudio
- ChatScreen onPlaybackFinished → stopBackgroundAudio
- audio.ts stopPlayback ruft auch stopBackgroundAudio damit die
  Notification bei Cancel/Barge-In/Anruf nicht haengen bleibt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:37:46 +02:00
duffyduckandClaude Opus 4.7 0879a0f4fd fix(wake-word): manueller Mikro-Stop beendet Konversation, zurueck zu armed
Bug: Nach Wake-Word "Computer" → conversing → User drueckt manuell den
Mikro-Button um zu stoppen → Audio wird gesendet, aber state bleibt
'conversing'. Nach ARIAs Antwort oeffnet sich automatisch wieder das
Mikro fuer Multi-Turn — obwohl der User explizit den Knopf gedrueckt
hat um zu signalisieren "ich bin fertig".

Fix: Im handleVoiceRecording (= manueller Stop ueber VoiceButton) wird
nach dem Send wakeWordService.endConversation() gerufen wenn aktuell
in conversing-State. Das setzt zurueck auf 'armed' und startet
openWakeWord wieder fuer passives Lauschen. ARIAs Antwort kommt durch,
TTS spielt, aber resume() ist dann no-op weil state schon 'armed'.

Bei VAD-Auto-Stop (silence-callback im Wake-Word-Pfad) bleibt das
Multi-Turn-Verhalten unveraendert — das ist die "natuerliche" Pause
und passt zum Konversations-Modus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:33:31 +02:00
duffyduckandClaude Opus 4.7 afb713364a feat: GPS-Position bei jeder Nachricht an aria-core (still, nur bei Bedarf)
App: GPS-Toggle in Settings → Allgemein → Standort wird jetzt korrekt
in AsyncStorage persistiert (key: aria_gps_enabled). ChatScreen pollt
den Wert mit den anderen Settings im 2s-Intervall.

Bridge: chat/audio-Handler nutzen jetzt einen gemeinsamen _build_core_text
Helper, der je nach Kontext einen Hint vorschaltet:
- Barge-In ("[Hinweis: Stefan hat dich unterbrochen ...]")
- GPS    ("[Stefans aktuelle GPS-Position: lat, lon. Nutze die nur wenn
          die Frage sich auf seinen Standort bezieht. Erwaehne sie nicht
          von dir aus, ausser er fragt explizit danach.]")

ARIA weiss bei "wo bin ich?" / "Wetter hier?" automatisch was zu tun ist
— bei normalen Fragen kommt die Position aber nicht ungefragt vor. Der
User sieht im Chat-Verlauf nichts von der GPS-Info, nur ARIAs Antwort
kann darauf eingehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:29:34 +02:00
duffyduck 6426d3866a release: bump version to 0.0.8.0 2026-05-06 23:06:22 +02:00
duffyduckandClaude Opus 4.7 fb6bf18d76 fix(vad): adaptive Baseline robuster — minimum + Cap-Bereich
Bug: Wenn beim Aufnahmestart sofort gesprochen wurde (z.B. Wake-Word-
Echo noch im Mikro) ODER der Hintergrund vorruebergehend laut war,
verschob die avg-basierte Baseline die Stille-Schwelle so weit nach
oben, dass normale Hintergrundgeraeusche dauerhaft als "Sprache"
zaehlten — VAD feuerte nie, Aufnahme lief unendlich.

Fix:
- Baseline = MINIMUM der 5 Samples statt Mittelwert (ruhigster Moment)
- Cap auf sinnvollen Bereich:
  - Silence-Schwelle: -50dB bis -28dB (vorher unbegrenzt)
  - Speech-Schwelle:  -40dB bis -18dB
- Erweitertes Log: zeigt sowohl raw als auch geclamp-te Werte

Damit gibt's keine "tote" VAD-Konfiguration mehr — selbst wenn die
Baseline-Messung Schrott ist, bleiben die Schwellen praktikabel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:05:08 +02:00
duffyduck ca055ddc6a release: bump version to 0.0.7.9 2026-05-06 23:00:32 +02:00
duffyduckandClaude Opus 4.7 200ee20d60 fix(audio): STT-Cleanup-Timeout skaliert mit Aufnahmedauer
Der pauschale 30s-Timeout vom Vorgaenger-Commit haette bei einer
5-Minuten-Aufnahme schon getriggert waehrend Whisper noch transkribiert
(Whisper braucht auf der Gamebox-GPU grob real-time/5, plus Bridge-
Roundtrip).

Neue Formel: 60s Buffer + 1x Aufnahmedauer.
- 5s Aufnahme → 65s Wait
- 5min Aufnahme → 6 min Wait
- 30min Aufnahme → 31 min Wait

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:59:20 +02:00
duffyduck 0f9a951964 release: bump version to 0.0.7.8 2026-05-06 22:58:20 +02:00
duffyduckandClaude Opus 4.7 dff6b958c0 fix(audio): Auto-Removal von Sprachnachrichten ohne STT-Result nach 30s
Bug: Wenn eine Aufnahme leer war, nur Wake-Word-Echo enthielt oder STT
sonstwie nichts erkannt hat, sendet die Bridge KEIN stt-Event zurueck —
die Placeholder-Bubble "Spracheingabe wird verarbeitet" blieb fuer immer
im Chat. Folge-Aufnahmen matchten dann via Substring-Fallback die ALTE
Placeholder, der echte Text landete in der falschen Bubble.

Fix: nach jedem audio-send einen 30s-Timer starten. Wenn nach Ablauf die
Bubble (per audioRequestId identifiziert) immer noch "verarbeitet" ist,
wird sie entfernt + Toast "nicht erkannt" zeigt das dem User.

So bleibt der State sauber + audioRequestId-Match auf zukuenftige
Aufnahmen findet die richtige Bubble (statt die hinterbliebene Placeholder).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:57:20 +02:00
duffyduck ee89277e0d release: bump version to 0.0.7.7 2026-05-06 22:52:23 +02:00
duffyduckandClaude Opus 4.7 92550aca07 feat(audio): Wake-Word parallel zu TTS mit AcousticEchoCanceler
Du kannst jetzt "Computer" sagen waehrend ARIA noch redet — TTS
verstummt, neue Aufnahme startet. Vorher musste man warten oder
manuell den Voice-Button tappen.

Native (OpenWakeWordModule.kt):
- AudioRecord-Source von MIC auf VOICE_COMMUNICATION (aktiviert auf
  den meisten Geraeten Echo-Cancellation + Noise-Suppression)
- Zusaetzlich AcousticEchoCanceler/NoiseSuppressor/AutomaticGainControl
  explizit aktiviert wenn vorhanden — robuster auf Geraeten wo die
  VOICE_COMMUNICATION-Source die Effects nicht automatisch mitbringt
- releaseAudioEffects() im stop/dispose

JS (wakeword.ts):
- Neue API: startBargeListening / stopBargeListening — Wake-Word
  parallel aktivieren, ohne den State 'conversing' zu verlassen
- onWakeDetected unterscheidet jetzt: in 'conversing' → barge-in-
  Callback (nicht der normale wake-callback). Sonst Standard-Pfad.
- onBargeIn-Subscriber-API + isBargeListening-Getter

Lifecycle-Wiring (audio.ts + ChatScreen):
- audioService.onPlaybackStarted callback (neu)
- ChatScreen: Bei TTS-Start → wakeWord.startBargeListening
- ChatScreen: Bei TTS-Ende → wakeWord.stopBargeListening (sonst kein
  AudioRecord fuer die naechste Aufnahme)
- ChatScreen: Bei BargeIn → haltAllPlayback + cancel_request +
  150ms-Pause + neue Aufnahme starten

issue.md + README aktualisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:50:09 +02:00
duffyduckandClaude Opus 4.7 2220d0efe7 docs: issue.md + README mit audioRequestId-Fix + Bereit-Sound aktualisiert
issue.md: drei neue Erledigt-Eintraege (Placeholder-Race per
audioRequestId, Mikro-Offen-Toast erst nach Recording-Start, Bereit-
Sound mit Toggle). Neuer Offen-Eintrag: Wake-Word parallel zu TTS
mit AcousticEchoCanceler.

README: Wake-Word-Bedienung erweitert um Ding-Dong + "🎤 sprich
jetzt"-Toast. Roadmap mit den beiden neuen Features ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:44:03 +02:00
duffyduck f80a17bbcd release: bump version to 0.0.7.6 2026-05-06 22:41:55 +02:00
duffyduckandClaude Opus 4.7 435b28ed78 feat(audio): "Bereit"-Sound (Ding-Dong) wenn Mikro nach Wake-Word offen ist
Kurzer akustischer Hinweis (Airplane Ding-Dong, 20KB MP3) bei
audioService.startRecording-Erfolg im Wake-Word-Pfad — User weiss
exakt ab wann er reden darf, statt das Toast nur zu sehen.

Quelldatei: android/sounds/Airplane-ding-dong.mp2 → ffmpeg-konvertiert
zu MP3 64kbps, abgelegt in android/app/src/main/res/raw/ damit Android
sie als Resource laden kann.

Toggle in App-Settings → Wake-Word, default aktiv. Bei Aktivierung
spielt direkt eine Vorschau ab damit man weiss wie's klingt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:40:45 +02:00
duffyduckandClaude Opus 4.7 b065e3c370 fix(audio): Placeholder-Race per audioRequestId + Mikro-Offen-Toast erst nach Start
Bug: Bei zwei Sprachnachrichten kurz hintereinander wurde der STT-Text
der zweiten in die Bubble der ersten geschrieben. Ursache: findIndex
matchte ueber Substring "Spracheingabe wird verarbeitet" → bei zwei
offenen Placeholders nahm er immer die ERSTE, egal welches STT-Result
gerade kam.

Fix: jede Aufnahme bekommt eine eindeutige audioRequestId, App pusht
sie in die Placeholder-Bubble + ans audio-Event. Bridge gibt sie
unveraendert ans STT-Result zurueck. App matcht primaer per ID, fallback
auf Substring (Kompatibilitaet zu alten Bridge-Versionen).

Bonus: Toast "Wake-Word erkannt" entfernt, dafuer "🎤 Mikro offen —
sprich jetzt" erst wenn audioService.startRecording wirklich erfolgreich
war. So weiss der User exakt ab wann er reden darf — vorher war der Toast
schon ~400ms vorher da.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:33:26 +02:00
duffyduck 5e66c65a3f release: bump version to 0.0.7.5 2026-05-06 22:15:49 +02:00
duffyduckandClaude Opus 4.7 9c19ef12f1 docs: issue.md + README aktualisiert mit aktuellen Features
issue.md: openWakeWord, ABI-Split, Underrun-Schutz, Conversation-Focus,
PhoneStateListener, Voice-Override-Fix, Bild+Text-Merge, Diagnostic-UI,
adaptive VAD, Max-Aufnahme konfigurierbar, Barge-In, Push-to-Talk-Refactor,
Settings-Sub-Screens, Textauswahl-Fix in Erledigt verschoben.
Porcupine-bezogene offene Bugs entfernt (Engine gewechselt). Neue Offene:
STT-Placeholder-Replacement, Custom-onnx-Upload, Pause+Resume bei Anruf.

README: Push-to-Talk-Erwaehnung raus, VAD-Beschreibung auf adaptiv +
neuen Default 5min, neue Bullets fuer Barge-In + Anruf-Pause, Roadmap
ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:13:53 +02:00
duffyduckandClaude Opus 4.7 f3212c920d feat(settings): Sub-Screen-Navigation statt langer Liste
Settings ist jetzt ein Hauptmenue mit 8 Kategorien — beim Tippen klappt
nur die gewaehlte Sektion auf, "<-Back-Button kehrt zur Uebersicht zurueck.

Gruppen:
- 🔌 Verbindung      (Server, Token, Status, Verbindungslog)
- ⚙️ Allgemein       (Betriebsmodus, GPS-Standort)
- 🎙️ Spracheingabe   (Stille-Toleranz, Aufnahmedauer)
- 👂 Wake-Word       (Wake-Word-Auswahl)
- 🔊 Sprachausgabe   (Stimmen, Pre-Roll, Geschwindigkeit)
- 📁 Speicher        (Anhang-Speicherort, Auto-Download)
- 📜 Protokoll       (Privatsphaere, Backup)
- ℹ️ Ueber           (App-Version, Update)

Implementierung absichtlich ohne react-navigation-Stack: ein einzelner
currentSection-State, conditional rendering pro Sektion. So bleibt aller
geteilte State (rvs.config, voices-Liste, Toggles) im selben Component-
Closure ohne props-drilling oder Context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:11:08 +02:00
duffyduckandClaude Opus 4.7 8c9b30c8d5 refactor(voice): Push-to-Talk-Pfad raus, nur Tap-to-Talk
handlePressIn/Out + onResponderGrant/Release/Terminate weg. Push-to-
Talk lief parallel zu Tap-to-Talk und triggerte je nach Touch-Timing
unkontrollierbar. Stefan kennt das Verhalten ohnehin nicht (sagt
"druecken startet, druecken stoppt") — Push-to-Talk macht UX nur
unklarer ohne Mehrwert.

isLongPress-Ref entfernt (war nur fuer Push-to-Talk-Discrimination).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:02:21 +02:00
duffyduckandClaude Opus 4.7 bb7de8e102 feat: Max-Aufnahmedauer konfigurierbar + Barge-In gibt aria-core Kontext
Max-Aufnahme:
Default rauf von 2 auf 5 Minuten, in den App-Settings konfigurierbar
zwischen 1 und 30 Minuten (loadMaxRecordingMs aus AsyncStorage,
Storage-Key aria_max_recording_sec). Notbremse-Verhalten bleibt:
nach Ablauf wird die Aufnahme automatisch beendet und gesendet.

Barge-In Kontext:
Wenn der User waehrend ARIA noch redet/arbeitet eine neue Sprach-
oder Text-Nachricht sendet, geht jetzt ein 'interrupted: true' Flag
mit. Bridge praefixed den Text fuer aria-core dann mit:

  "[Hinweis: Stefan hat dich gerade unterbrochen waehrend du noch
  gesprochen oder gearbeitet hast. Folgendes ist eine Korrektur,
  Ergaenzung oder ein Themenwechsel zu deiner letzten Antwort.]"

So weiss ARIA dass die neue Message KEINE eigenstaendige Folgefrage
ist sondern auf den abgebrochenen Run bezogen. Der User sieht in
seinem Chat nur den reinen Text — der Hint geht nur an aria-core.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:58:11 +02:00
duffyduckandClaude Opus 4.7 2efdc2c6a9 fix: Textauswahl, adaptive VAD-Schwelle + Barge-In bei Sprachaufnahme
Bug 1 — Textauswahl in Bubbles ging nicht mehr:
MessageText hatte verschachtelte <Text onPress={...}> fuer Custom-Link-
Styling. Das fing die Long-Press-Geste ab, daher kein Markieren+Kopieren
mehr. Jetzt nur noch ein einzelnes <Text selectable dataDetectorType="all">,
Android macht URLs/Telefonnummern/Emails per System-Detection klickbar.

Bug 2 — VAD erkannte Stille nicht zuverlaessig (Aufnahme lief endlos):
Festwerte (-45dB Stille / -28dB Sprache) passten nicht zu jeder Umgebung.
In lauteren Raeumen lag der Hintergrundpegel ueber der Stille-Schwelle,
lastSpeechTime wurde dauerhaft aktualisiert → VAD feuerte nie, Aufnahme
lief bis 120s Max-Duration.

Jetzt adaptiv: erste 5 Mic-Samples (~500ms) bilden die Baseline; Stille-
Schwelle = baseline+6dB, Sprache-Schwelle = baseline+12dB. Toast zeigt
die kalibrierten Werte beim Aufnahmestart. Fallback auf -38dB/-22dB falls
das Mikro keine Metering-Updates liefert.

Bug 3 — Barge-In ("ach vergiss es"):
Wenn waehrend ARIAs Antwort eine neue Sprachnachricht aufgenommen wird,
wird ARIAs aktuelle Aktivitaet (TTS + thinking/tool) sofort abgebrochen
bevor die neue Message gesendet wird — wie in einem echten Gespraech wo
man den anderen unterbrechen darf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:49:48 +02:00
duffyduck ab0b224370 release: bump version to 0.0.7.4 2026-05-06 20:30:37 +02:00
duffyduckandClaude Opus 4.7 864de803de debug(stt): Toasts in App + Bridge-Log fuer STT-Broadcast-Erfolg
Da kein adb-Zugriff: visuelle Debug-Pfade direkt in der App + im
Diagnostic-Bridge-Tab.

App: zwei Toasts beim Empfang eines stt-events
- "STT empfangen: ..." sobald das chat-event mit sender=stt reinkommt
- "Bubble #X ersetzt" oder "keine Placeholder → neue Bubble"

Bridge: explizites Info-Log "STT-Text an RVS broadcastet (sender=stt)"
nach erfolgreichem _send_to_rvs, "NICHT broadcastet" wenn die Methode
False lieferte (Ping fehlgeschlagen / Verbindung tot).

Naechster Test:
- Sprachnachricht aufnehmen
- Toast erscheint? → STT-Event kommt in App an, Bug ist im findIndex
- Toast erscheint nicht? → Diagnostic Bridge-Tab pruefen ob das Log
  "STT-Text an RVS broadcastet" steht

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:29:26 +02:00
duffyduck e71a1b8374 release: bump version to 0.0.7.3 2026-05-06 20:12:01 +02:00
duffyduck 7c3330287c release: bump version to 0.0.7.2 2026-05-05 14:51:18 +02:00
duffyduckandClaude Opus 4.7 7c5b963372 fix(compose): $(find ...) muss in compose-command zu $$(find ...) escaped werden
docker-compose interpretiert $( als Variable-Interpolation-Pattern und
warf "Invalid interpolation format". Die anderen $$DIST-Stellen waren
schon korrekt escaped, nur das command-substitution fehlte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:24:31 +02:00
duffyduckandClaude Opus 4.7 d15c29ad14 fix(diagnostic+app): Chat-UI bubblig, mehrzeilig + persistente RVS + STT-Logs
Diagnostic-UI:
- chat-msg ist jetzt eine richtige Bubble (border-radius 14px, Schatten,
  flex-Layout statt margin-Hack, Tail-Radius zur Sender-Seite hin).
- Eingabefelder (haupt + Vollbild) jetzt textarea mit Auto-Resize.
  Enter sendet, Shift+Enter macht neue Zeile.
- white-space: pre-wrap behaelt Zeilenumbrueche aus dem Text bei.

Diagnostic-Server:
- sendToRVS_raw nutzt jetzt die persistente rvsWs statt fuer jedes Send
  eine frische Verbindung aufzubauen. Der frische-WS-Pfad hatte Race-
  Probleme (WS schloss bevor RVS broadcasten konnte → User-Nachrichten
  von Diagnostic kamen nicht in der App an). Frische WS bleibt als
  Fallback wenn die persistente gerade tot ist.

App:
- console.log am Anfang des chat-handlers + im STT-Result-Handler mit
  findIndex-Result und Placeholder-Count. Bei nicht-erkanntem STT-Text
  liefert `adb logcat -s ReactNativeJS:V` jetzt direkt den Befund:
  kommt das Event ueberhaupt an, findet er die Placeholder?

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:19:16 +02:00
duffyduck 41d8faa045 release: bump version to 0.0.7.1 2026-05-03 22:11:08 +02:00
duffyduck fceae95a3c release: bump version to 0.0.7.0 2026-05-03 21:59:38 +02:00
duffyduckandClaude Opus 4.7 bd6f1b24e3 feat(audio): TTS pausiert bei Anruf + Conversation-Focus haelt Spotify durchgehend gepaust
Bug 1a — Anruf-Pause:
Neues PhoneCallModule.kt nutzt TelephonyCallback (API 31+) bzw.
PhoneStateListener (Pre-12) um auf RINGING/OFFHOOK/IDLE zu reagieren.
Bei Klingeln/Gespraech ruft phoneCall.ts → audioService.haltAllPlayback,
ARIA verstummt sofort. READ_PHONE_STATE Permission wird beim ersten
Start angefragt; ohne Permission failt der Listener leise.

Bug 1b — Spotify-Resume:
AudioFocus wird jetzt an den Conversation-Lifecycle gekoppelt statt an
einzelne Streams. Solange wakeWordState 'conversing' ist, blockt
acquireConversationFocus() jeden per-Stream-Release. Erst beim Wechsel
auf 'armed'/'off' darf der Focus tatsaechlich freigegeben werden.
Verhindert das "Spotify kommt nach 10s wieder hoch"-Phaenomen auch
ueber Render-Pausen + zwischen mehreren ARIA-Antworten hinweg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:44:58 +02:00
duffyduckandClaude Opus 4.7 f8dee3d6e2 fix: Sprachnachricht-Bubble defensiv + Bild+Text als eine Anfrage
Bug 2: STT-Result schreibt jetzt eine neue User-Bubble wenn keine
Placeholder im State gefunden wird (statt das Update zu verwerfen).
Schuetzt vor Race-Conditions zwischen audio-send und State-Updates,
damit der gesprochene Text immer im Chat erscheint.

Bug 3: Bild + Text wurden als zwei getrennte Events ('file' + 'chat')
gesendet, jeder triggerte einen eigenen send_to_core. ARIA antwortete
zweimal — einmal "warte auf Anweisung" beim Bild, dann nochmal auf
den Text. Bridge buffert jetzt eingehende file-Events 800ms; kommt in
dem Fenster ein chat, werden alle Files + Text zu einer einzigen
aria-core-Nachricht gemerged. Kein chat → Files alleine wie bisher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:40:15 +02:00
duffyduck b0774c697e release: bump version to 0.0.6.9 2026-05-01 00:08:08 +02:00
duffyduckandClaude Opus 4.7 862c677603 fix(voice): Stimmen-Wechsel greift wieder — Override bleibt bis naechster Chat-Event
Bug: Voice-Override wurde nach der ersten ARIA-Antwort konsumiert.
Eine ARIA-Antwort triggert aber oft mehrere TTS-Calls (Tool-Use →
Zwischenmeldung → finale Antwort). Der erste nutzte die neue Stimme,
alle folgenden fielen auf self.xtts_voice (= alte Voice aus
voice_config.json) zurueck. Die App schickt nie ein config-Update,
daher blieb voice_config.json fuer immer auf der alten Stimme.

Neue Semantik:
- chat-/audio-Event mit voice="X" → Override="X", gilt fuer alle
  folgenden TTS-Calls bis zum naechsten chat-Event
- chat-Event mit voice="" → Override geloescht, fallback auf
  Default-Voice (voice_config.json / Diagnostic)
- chat-Event ohne voice-Field → Override unveraendert

Audio-Send in ChatScreen.tsx (Push-to-Talk-Pfad) gab voice/speed
gar nicht mit; jetzt konsistent mit dem Tap-to-Talk-Pfad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:04:19 +02:00
duffyduck 759541d248 release: bump version to 0.0.6.8 2026-04-26 13:26:00 +02:00
duffyduckandClaude Opus 4.7 e8963326b0 fix(wake-word): Embedding-Output ist rank-4, nicht rank-2 — Trigger funktioniert jetzt
Hauptursache warum kein Wake-Word je triggerte: das Google-Speech-
Embedding-Modell liefert (1,1,1,96), nicht (1,96). Der Cast
`as Array<FloatArray>` warf eine ClassCastException, die vom try/catch
geschluckt wurde — Pipeline lief still ins Leere.

Zusaetzlich:
- WW-Input-Frame-Count wird jetzt aus den Modell-Metadaten gelesen
  (variiert pro Keyword; hey_jarvis=16, computer_v2evtl. anders)
- "Computer" als Wake-Word erweitert (Community-Modell aus
  fwartner/home-assistant-wakewords-collection)

"ARIA" als Wake-Word: gibt's nicht fertig trainiert. Muesste ueber
das openWakeWord Colab-Notebook trainiert werden (~1h auf gratis-GPU).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:24:47 +02:00
duffyduckandClaude Opus 4.7 4bd8fde71c fix(app): Underrun-Schutz im PcmStreamPlayer — Spotify resumed nicht mehr nach 10s
Wenn die Bridge zwischen zwei Saetzen rendert (1-2s pro Satz auf der
Gamebox-RTX 3060), kommen keine neuen PCM-Chunks rein und der AudioTrack-
Buffer laeuft leer. Spotify hat eine eigene Heuristik die nach ~10s
"stummer Lücke" eigenmaechtig die Wiedergabe wiederaufnimmt — auch wenn
wir den AudioFocus formal noch halten.

Fix: Writer-Thread fuettert Stille rein wenn der Puffer unter ~100ms
faellt (~50ms pro Refill-Tick alle 50ms). AudioTrack bleibt damit
durchgehend aktiv, andere Apps respektieren weiterhin den Fokus.

Bonus: 30s-Idle-Cutoff falls die Bridge crashed und kein final-Marker
mehr kommt — sonst wuerde der Writer-Thread ewig Stille fuettern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:18:25 +02:00
duffyduck 7eb873aa4e release: bump version to 0.0.6.7 2026-04-26 13:08:13 +02:00
duffyduckandClaude Opus 4.7 b30f0f613d build(android): ABI-Split auf arm64-v8a — APK von 136 MB auf ~35 MB
Mit ONNX Runtime fuer das Wake-Word kommen Native-Libs fuer alle 4
Architekturen rein (arm64-v8a, armeabi-v7a, x86, x86_64). Das
sprengt sowohl den Gitea-Upload (nginx-Limit) als auch unnoetig die
Auto-Update-Downloads aufs Phone. Per ABI-Split jetzt nur noch
arm64-v8a — deckt jedes Android-Phone seit 2017 ab.

build.sh greift den neuen APK-Pfad (app-arm64-v8a-release.apk),
faellt auf app-release.apk zurueck falls die Splits in build.gradle
deaktiviert werden.

versionCode 606 / versionName 0.0.6.6 (vom Linter mitgehoben).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:04:32 +02:00
duffyduck 40a8e14b52 release: bump version to 0.0.6.6 2026-04-26 12:59:26 +02:00
duffyduckandClaude Opus 4.7 8553442615 feat(app): Wake-Word komplett on-device via openWakeWord (ONNX)
Picovoice/Porcupine raus — neuer Stack ist openWakeWord (Apache 2.0,
on-device, ONNX Runtime). Kein API-Key, keine Lizenzgebuehren, Audio
verlaesst das Geraet nicht. Eigene Wake-Words sind via openWakeWord-
Notebook gratis trainierbar.

Pipeline (alles im OpenWakeWordModule.kt):
  1. AudioRecord 16kHz mono int16 in 1280-Sample-Chunks (80ms)
  2. melspectrogram.onnx → 32-mel Frames (mel/10 + 2 wie in Python)
  3. embedding_model.onnx, 76-Frame Sliding Window (stride 8) → 96-dim
  4. hey_jarvis.onnx (oder anderes Keyword) auf letzten 16 Embeddings
  5. Sigmoid-Score, threshold/patience/debounce-Filter
  6. RN-Event "WakeWordDetected" raus

Mitgelieferte Modelle in assets/openwakeword/: hey_jarvis (Default),
alexa, hey_mycroft, hey_rhasspy. Externe Service-API (start/stop/
configure/onWakeWord/...) bleibt identisch — ChatScreen unveraendert.

build.gradle: com.microsoft.onnxruntime:onnxruntime-android:1.17.1
package.json: @picovoice/porcupine-react-native + voice-processor raus
SettingsScreen: AccessKey-Feld weg, neue Keyword-Liste mit Labels
README: Wake-Word-Sektion komplett umgeschrieben (kein Picovoice mehr)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:56:33 +02:00
duffyduck 41909f867a release: bump version to 0.0.6.5 2026-04-25 22:51:00 +02:00
duffyduckandClaude Opus 4.7 770fdf72fa fix(app): Spotify-Bounce zwischen ARIA-Antworten + Wake-Word-Doku
AudioFocus wird jetzt mit 800ms Verzoegerung freigegeben — wenn ARIA
direkt eine zweite Antwort hinterherschickt oder das Recording ins TTS
uebergeht, wird das Release abgebrochen. Spotify/YouTube haben damit
keine Mikro-Sekunden-Luecke mehr zum Hochkommen waehrend ARIA spricht.

README: neue Sektion zur Wake-Word-Einrichtung mit Picovoice
(7-Tage-Trial, Console-Link, Anleitung fuer eigene Keywords) und
veraltete Wake-Word-Limitation entfernt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:49:45 +02:00
duffyduck 26bc617bb6 release: bump version to 0.0.6.4 2026-04-25 20:58:10 +02:00
duffyduckandClaude Opus 4.7 d8d3eaa3ba debug: explicit speed-Log direkt vor F5-TTS infer()-Call
Damit man am Logfile schwarz auf weiss sieht ob der Wert wirklich an
die Library geht — falls bei Stefan speed=0.30 ankommt aber Maia
trotzdem schnell, ist's F5-TTS-Verhalten, nicht Pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:45:16 +02:00
duffyduckandClaude Opus 4.7 25eb6f5659 fix: Text-Auswahl in MessageText — selectable an alle nested Texts
Android-Eigenheit: bei nested Text-Komponenten muss selectable=true
auch an die Kinder; der Wert auf dem Parent erbt sich nicht zuverlaessig.
Plus: dataDetectorType="all" als Fallback fuer System-Linkifizierung,
falls unsere Regex einen Match verpasst.

suppressHighlighting=false damit Long-Press auf den Link-Texten den
Selection-Mode nicht blockt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:41:20 +02:00
duffyduck 1f16bac3b8 release: bump version to 0.0.6.3 2026-04-25 20:35:25 +02:00
duffyduckandClaude Opus 4.7 8c5dcfe751 fix: klares UI-Feedback fuer Wake-Word-State
Stefan's Verwirrung: Ohr-Button + KEIN Porcupine = Direkt-Aufnahme,
nicht passives Lauschen. Wenn er lange wartet, schnappt das Mikro
Hintergrundgeraeusche/Sprache auf, sendet ab, Ohr aus. Sah aus wie
"Wake-Word triggerte" — war aber stinknormales Recording.

Fixes fuer klares Feedback:
- Toast bei jedem State-Wechsel:
  * Direkt-Aufnahme (kein Porcupine): "Wake-Word nicht aktiv —
    direkte Aufnahme startet (Mikro hoert mit)"
  * armed: "Lausche auf X..."
  * Wake erkannt: "Wake-Word X erkannt — sprich jetzt"
  * endConversation: "Lausche wieder auf X" oder "Mikro aus"
- Ohr-Button-Icon zeigt drei States:
  🔇 off
  👂 armed (Porcupine lauscht passiv)
  🎙️ conversing (aktive Aufnahme laeuft)
- ChatScreen subscribed wakeWordService.onStateChange fuer Live-
  Updates des Icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:34:07 +02:00
duffyduckandClaude Opus 4.7 e241bde92e fix: 4 Bugs — STT-Mapping, Speed-Logging, VAD-Logs, Wake-Word-Toast
Bug 2: STT-Result ueberschrieb beide noch unaufgeloeste Audio-Bubbles
mit gleichem Text. Fix: nur die ERSTE matchende Bubble aktualisieren
(findIndex + index-Update statt map). Reihenfolge ist FIFO weil Whisper
sequenziell verarbeitet.

Bug 3: Speed-Param wird nun in jedem Hop geloggt:
  - ChatScreen: "[Chat] sende mit voice=X speed=Y"
  - aria-bridge: "XTTS-Request gesendet (voice=X, speed=Y.YYx)"
  - f5tts-bridge: "F5-TTS: N Satz(e), voice=X, speed=Y.YYx"
Damit kann man im logcat/docker-logs eindeutig sehen wo speed evtl.
verloren geht oder ob die Stimme einfach von Natur aus schnell ist.

Bug 4: VAD-Trigger-Reason mit Schwelle: "VAD NNN ms Stille (Schwelle=NNN ms)".
Plus startRecording loggt jetzt VAD-Stille + MAX-Recording.

Bug 1 (Porcupine): mehr Debug + Toast-Meldungen.
  - init failure: err.name/code/stack ins Log
  - start() ohne Porcupine: Toast "Access Key in Settings setzen"
  - start() Fehler: Toast mit Fehlermeldung
  - configure(): Toast wenn init scheitert
  - Erfolgreiches arming: Toast "Lausche auf X"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:28:46 +02:00
duffyduck 52b1414d35 release: bump version to 0.0.6.2 2026-04-25 20:20:25 +02:00
duffyduckandClaude Opus 4.7 94a5d6714a fix: handlePcmChunk serialisiert — fixes Race bei kurzen Streams
Bei kurzen Saetzen (nur ein paar Chunks + sofort final) konnten die
async handlePcmChunk-Calls parallel laufen. Der final-Chunk konnte
native end() aufrufen BEVOR der vorherige Chunk seinen native start()
abgeschlossen hatte. Der Writer-Thread startete dann mit endRequested
bereits true, verarbeitete keine Chunks sauber → Audio ging verloren.

Fix: Wrapper chaint alle Chunk-Calls an eine Promise-Queue:
  _pcmChunkQueue = Promise.resolve()
  handlePcmChunk → _pcmChunkQueue.then(() => _handlePcmChunkImpl(p))

So werden start/writeChunk/end garantiert in der richtigen Reihenfolge
verarbeitet. Der API-Contract bleibt (gleiches return-Promise).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:58:27 +02:00
duffyduck 1456f01429 release: bump version to 0.0.6.1 2026-04-25 01:24:31 +02:00
duffyduckandClaude Opus 4.7 75d4c90c84 fix: alte APKs im Cache werden jetzt aufgeraeumt
Die heruntergeladenen Update-APKs (~20-30MB pro Release) landeten in
CachesDirectoryPath und wurden nie geloescht. Bei regelmaessigen
Updates sammelt sich das auf mehrere 100MB an.

Fix: cleanupOldApks() wird gerufen
  - einmal beim App-Start (Constructor) — alte APKs sind sowieso nicht
    mehr relevant, die aktuelle Version laeuft ja aus dem System
  - vor jedem neuen Download — falls jemand zwei Updates in einer
    Session zieht

Loescht alle *.apk Dateien im CachesDirectoryPath und loggt die
freigemachte Groesse pro Datei.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:22:22 +02:00
duffyduck 94d086005d release: bump version to 0.0.6.0 2026-04-25 01:13:42 +02:00
duffyduckandClaude Opus 4.7 cb15739648 fix: preroll=0 wirklich sofort + Trailing-Silence gegen Wort-Cutoff
Zwei Bugs die zusammen dafuer sorgen dass Worte "verschluckt" werden:

1) play() wurde bei preroll=0 erst beim ersten echten Chunk aufgerufen
   — nicht schon nach der Leading-Silence. Dadurch musste AudioTrack
   gleichzeitig Startup UND Audio abspielen, die Hardware-Anfahr-Latenz
   schluckt die ersten Samples.

   Fix: Bei prerollBytes==0 direkt nach dem silence-write play() rufen.
   AudioTrack haelt den Play-State und wartet auf mehr Samples — die
   naechsten Chunks kommen in den bereits laufenden Stream rein.

2) Nach letztem Chunk ging der Writer via return@Thread in den finally-
   Block. Der wartete zwar auf playbackHeadPosition >= totalFrames, aber
   Android's Hardware-Pipeline puffert oft noch ein paar Samples nach —
   stop() kam, Samples futsch.

   Fix: 300ms TRAILING_SILENCE am Ende schreiben. playbackHeadPosition
   erreicht echt bis zum Ende der echten Samples bevor die Stille abspielt.
   Loop umgeschrieben auf mainLoop-Label (break statt return@Thread) damit
   Trailing-Silence garantiert laeuft.

LEADING_SILENCE auf 300ms erhoeht fuer bessere AudioTrack-Warmup-Toleranz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:11:23 +02:00
duffyduck c89ef00759 release: bump version to 0.0.5.9 2026-04-25 01:04:00 +02:00
duffyduckandClaude Opus 4.7 3990aeaf74 feat: Pre-Roll-Buffer kann jetzt auf 0 (sofort abspielen)
F5-TTS ist schnell genug dass der Puffer bei kurzen Saetzen eher
schadet als nuetzt — er verzoegert den play()-Start fuer Sekunden die
dann als Wartezeit auffallen.

Aenderungen:
- audio.ts: TTS_PREROLL_MIN_SEC 1.0 → 0 (Einstellbar in Settings)
- PcmStreamPlayerModule.kt: MIN_PREROLL_SECONDS auf 0.0, Fallback-
  Logic respektiert jetzt 0 als gueltigen Wert (vorher hat der
  .let { if (it > 0) it else DEFAULT } 0 zu 3.5s umgebogen).

Bei preroll=0 greift der Leading-Silence von 200ms immer noch, d.h.
AudioTrack-Startup bleibt sauber. play() wird dann beim allerersten
echten PCM-Chunk aufgerufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:02:48 +02:00
duffyduckandClaude Opus 4.7 6e0a4fd022 feat(diagnostic): Speed-Slider im Voice-Preview-Modal (nicht persistiert)
Neue −0.1 / +0.1 Buttons im Preview-Modal mit aktuellem Wert-Label.
Bei jedem Oeffnen wird der Speed auf 1.0 zurueckgesetzt (bewusst kein
persist — nur zum Experimentieren waehrend das Modal offen ist).

- Range 0.1-5.0, gleiche wie in App-Settings
- Wird beim Play an f5tts-bridge als speed-Param mitgegeben
- Server clampt auf 0.1-5.0, Fallback 1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:54:40 +02:00
duffyduck c9068bf7ba release: bump version to 0.0.5.8 2026-04-25 00:51:13 +02:00
duffyduckandClaude Opus 4.7 7db944312e feat: Sprechgeschwindigkeit-Range auf 0.1-5.0 erweitert
TTS_SPEED_MIN 0.5 → 0.1, TTS_SPEED_MAX 2.0 → 5.0.
Bridge-seitige Validierungen (aria_bridge.py + f5tts/bridge.py) mit-
gezogen auf den gleichen Bereich.

Hinweis: Extremwerte (unter 0.5 oder ueber 2.0) koennen bei F5-TTS
verzerrte Ausgaben produzieren — Stefan bekommt die Freiheit zum
Experimentieren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:49:05 +02:00
duffyduckandClaude Opus 4.7 e304b156fb fix: Aufnahme-Crashes/Double-Tap durch VAD-Multi-Fire + stale closure
Drei zusammenhaengende Bugs:

1. VAD-Timer feuerte im 200ms setInterval WEITER nachdem die Stille-
   Schwelle erreicht war — listeners wurden pro Aufnahme bis zu 5x
   getriggert. Parallel laufende stopRecording()-Calls lieferten
   audio-recorder-player's nativen Layer OOM / Crash.

   Fix: silenceFired-Latch + Timer-Clear SOFORT beim ersten Feuer
   (fireSilenceOnce-Helper). Gleiche Logik fuer Max-Dauer + Conv-Window.

2. VoiceButton silence-listener re-registrierte bei jedem isRecording-
   Flip (deps [isRecording, onRecordingComplete]). Closure-State war
   stale, und bei schnellen flips gabs register/unregister-Races.

   Fix: empty deps, state direkt vom audioService via getRecordingState()
   lesen. onRecordingComplete via Ref (damit der Callback aktuell bleibt
   ohne re-register).

3. handleTap las den Button-State aus React (isRecording), der bei
   schnellen Taps stale sein konnte — "erst zweiter Tap geht" Symptom.

   Fix: audioService.getRecordingState() als Source-of-Truth, plus
   tapBusy-Ref als Anti-Doppel-Tap-Guard waehrend asyncer start/stop.
   'processing'-State wird korrekt ignoriert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:47:53 +02:00
duffyduckandClaude Opus 4.7 4dcc952911 debug: Log wenn Pre-Roll-Fallback bei kurzem Text greift
Stefan hat aufgeklaert: Auto-Playback geht nur bei LANGEN Saetzen, bei
kurzen nicht. Das passt zur Pre-Roll-Logik: wenn weniger als pre-roll
Bytes gepuffert werden, soll eigentlich der Fallback in end() greifen,
der nach queue-Timeout play() aufruft.

Neuer Log-Eintrag zeigt ob der Fallback ausgeloest wurde:
  "Playback gestartet VOR Pre-Roll (kurzer Text, NNNNB gepuffert)"

Beim naechsten Test mit adb logcat sehen wir direkt:
  * Fallback-Log kommt → play() wurde aufgerufen, Problem liegt woanders
  * Fallback-Log kommt NICHT → endRequested wird nicht rechtzeitig
    erkannt oder Race mit concurrent handlePcmChunk-Calls

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:42:28 +02:00
duffyduckandClaude Opus 4.7 1164c43eec debug: Logs fuer Auto-Playback-Bug — canPlay + silent-state sichtbar
Stefan berichtet dass Auto-Playback trotz Closure-Fix nicht greift.
Zwei neue Log-Zeilen die beim naechsten Test direkt zeigen was schief
laeuft:
  - ChatScreen: "[Chat] audio-msg canPlay=X (enabled=Y muted=Z)"
  - audio.ts:   "[Audio] PCM-Stream start: silent=X messageId=Y ..."

Ausreichend um zu unterscheiden:
  * canPlay=false trotz Mund-an → ttsMuted bleibt im State haengen
  * canPlay=true aber silent=true in audio.ts → Ref-Bug oder race
  * silent=false aber nichts hoerbar → native-module oder audio-routing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:38:22 +02:00
duffyduck 0324643257 release: bump version to 0.0.5.7 2026-04-25 00:31:12 +02:00
duffyduckandClaude Opus 4.7 9f8039a91c docs: NO_REPLY-Bug raus — schon durch NO_REPLY-Filter (Zeile 45) erledigt
War doppelt gelistet: einmal als erledigter Filter und einmal als
offener "wird als NO angezeigt"-Bug. Der Filter in aria-bridge verwirft
NO_REPLY-Antworten heute still, der Anzeige-Bug ist damit praktisch weg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:29:29 +02:00
duffyduckandClaude Opus 4.7 adc5ed5348 docs: issue.md aufgeraeumt — erledigte Items in die Done-Liste
Offene Bugs auf 3 reduziert (NO_REPLY-Anzeige, Porcupine-Jarvis,
Porcupine-Crash — die zwei letzten abhaengig vom ADB-Logcat-Test).
App-Features-Backlog auf 2 (History-Race, Background-Audio) weil
Text-Auswahl/Autolink/Speed-Setting/Voice-Preview jetzt fertig sind.

Diagnostic Features-Abschnitt leer → geloescht.

Erledigt-Liste um ~18 Punkte ergaenzt (F5-TTS Pre-processing, deutsches
Fine-Tune, maxPayload-Fix, service_status, config_request, Conversation-
Window, Porcupine, HF-Cache Bind-Mount, cleanup-windows, Mute-Bug,
Zombie-Recording, Autolink, Speed-Setting, Preview-Modal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:28:37 +02:00
duffyduckandClaude Opus 4.7 00f5c30354 docs: veraltete F5-TTS-Backlog-Items ausmisten
- Audio-Normalisierung: nie aufgefallen, bei Bedarf zurueck
- F5-TTS Streaming-Inferenz: Upstream-Feature, nicht unseres
- Deepspeed: premature optimization, Render ist durch Pre-Roll
  kaschiert schnell genug
- BigVGAN-Support: obsolet seit Vocos + aihpi German Fine-Tune laeuft

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:26:51 +02:00
duffyduckandClaude Opus 4.7 a3ab1965f8 feat: Bug-Runde + 5 App/Diagnostic-Features
Bugs:
- App Mute-/Auto-Playback: onMessage-Closure hielt stale ttsDeviceEnabled/
  ttsMuted → Mute wurde ignoriert + AsyncStorage-Load kam nicht durch.
  Fix via ttsCanPlayRef (live gespiegelt) statt Closure-Variablen.
- App Zombie-Recording: toggleWakeWord hat die laufende Aufnahme nicht
  gestoppt → audioService.recordingState blieb 'recording' → normaler
  Aufnahme-Button wirkungslos. Fix: await stopRecording() vor stop().
- Porcupine robuster: BuiltInKeywords-Enum Mapping mit String-Fallback,
  errorCallback fuer Runtime-Crashes (state zurueck auf off statt
  App-Crash), mehr Logging damit man beim naechsten Issue debuggen kann.

App-Features:
- MessageText Komponente: Text ist durchgehend selektierbar, erkennt
  URLs (http/https), E-Mails, Telefonnummern und macht sie anklickbar
  (oeffnet Browser / Mail-App / Android-Dialer via Linking).
- TTS-Wiedergabegeschwindigkeit pro Geraet einstellbar (Settings ->
  "Sprechgeschwindigkeit", 0.5-2.0 in 0.1-Schritten, Default 1.0).
  Wird als speed-Param an die F5-TTS-Bridge durchgereicht.

Bridge-Durchreichen:
- ChatScreen: speed aus AsyncStorage via ttsSpeedRef, an chat/audio/
  tts_request mitgeschickt
- aria-bridge: _next_speed_override wie voice_override, an xtts_request
  weitergereicht
- f5tts-bridge: speed-Param an F5TTS.infer() durchgereicht

Diagnostic-Feature:
- Voice-Preview-Button (Play-Icon) vor dem Delete-X in der Stimmen-Liste
- Modal mit Textfeld (Default-Beispieltext wird bei jedem Oeffnen neu
  gesetzt) und Play-Button
- Server sammelt audio_pcm Frames der Preview-Anfrage, baut WAV,
  schickt base64 zurueck, Browser spielt im <audio>-Tag ab
- 60s Timeout-Safety-Net

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:24:02 +02:00
duffyduckandClaude Opus 4.7 fba042fe6d fix: Leeres Feld im Diagnostic bedeutet jetzt wirklich "reset auf default"
Bug: User leert "Custom Checkpoint" in Diagnostic, klickt Anwenden, aber
die Bridge behielt den alten Wert weiter (z.B. BigVGAN-Pfad). Ursache:
  - Server loeschte den Key bei leerem String aus voice_config.json
  - Bridge's update_config sah key absent → "keep current" Semantik
  - Resultat: kein Reset, alter Pfad blieb aktiv, NaN-Output blieb

Fix auf beiden Seiten:
  - diagnostic/server.js: Keys werden immer mit dem Wert gesetzt (auch "")
    statt geloescht. "" landet jetzt explizit in der config.json.
  - f5tts/bridge.py: update_config unterscheidet jetzt:
      * key fehlt in payload  → current behalten (unveraendert)
      * key da + leer         → RESET auf DEFAULT_F5TTS_* (User-Wunsch)
      * key da + Wert         → neuen Wert nehmen

Damit kann der User in Diagnostic ein Feld leeren + Anwenden und die
Bridge schaltet wirklich auf Hard-Default zurueck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:51:10 +02:00
duffyduckandClaude Opus 4.7 3ca599b3ce fix: Voice-Transkription erzwingt kein "small" mehr — nutzt geladenes Modell
f5tts/bridge.py: das hardcoded model="small" in request_transcription war
ein Fehler — whisper-bridge hat dadurch stumm zwischen Modellen geswappt.
Wenn User large-v3 in Diagnostic eingestellt hatte:
  - f5tts Voice-Transkribierung triggerte Swap zu "small"  (+~500MB Laden)
  - Danach aria-bridge schickte naechsten stt_request mit large-v3
    (+~3GB Laden weil small jetzt im RAM war)
Doppelter Load, unnoetiger Traffic.

Fix:
  - f5tts: kein model mehr im payload, whisper-bridge entscheidet
  - whisper: wenn kein payload.model UND bereits ein Modell geladen →
    das behalten. Nur wenn gar nichts da ist fallback auf WHISPER_MODEL
    env default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:03:45 +02:00
duffyduckandClaude Opus 4.7 d209c4f2f4 docs: BigVGAN-Warnung deutlich — funktioniert nicht mit unserem Vocos-Setup
Die BigVGAN-Variante des aihpi F5-TTS Checkpoints ist nicht einfach ein
"optional besser" Fallback — sie ist mit dem Default-Vocos-Vocoder den die
f5-tts Library laedt inkompatibel. Output wird NaN, App bleibt stumm.

Stefan hat das probiert, App stumm, 10 Minuten Debugging. README war zu
locker formuliert ("Meist hoehere Quali") — jetzt klar als "funktioniert
AKTUELL NICHT" gekennzeichnet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:49:54 +02:00
duffyduckandClaude Opus 4.7 37beed7b0b fix(f5tts): NaN/Inf in Modell-Output sauber abfangen vor int16-Cast
F5-TTS generiert gelegentlich NaN/Inf samples — ohne sanitize lief der
int16-Cast in undefined behavior (RuntimeWarning + kaputter Sound in den
entsprechenden Stellen). Jetzt: nan_to_num vor clip, plus Warnung wie
viele samples betroffen waren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:35:57 +02:00
duffyduckandClaude Opus 4.7 ea129efe25 docs: README-Abschnitt fuer deutsches F5-TTS Fine-Tune (aihpi)
Konfig-Tabelle mit den konkreten Diagnostic-Werten fuer das deutsche
Fine-Tune von aihpi/F5-TTS-German — Modell-Architektur, hf:// Pfade,
empfohlene cfg_strength / nfe_step. Plus Hinweis auf die BigVGAN-
Variante als Alternative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:34:36 +02:00
duffyduckandClaude Opus 4.7 134b321895 feat(f5tts): HF-URL Support fuer Custom Checkpoints (aihpi/F5-TTS-German)
_resolve_hf_path wandelt hf://user/repo/path → lokaler Download via
huggingface_hub.hf_hub_download. So kann man in Diagnostic einfach die
HF-Pfade fuer custom Modelle reinschreiben, ohne erst manuell zu
downloaden + zu mounten.

Format: hf://aihpi/F5-TTS-German/F5TTS_Base/model_365000.safetensors
        hf://aihpi/F5-TTS-German/vocab.txt

Diagnostic UI: Placeholders + Labels angepasst mit Beispiel-HF-Pfaden
und Hinweis dass fuer Fine-Tunes "F5TTS_Base" statt "F5TTS_v1_Base"
als Architektur-Name gesetzt werden muss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:16:44 +02:00
duffyduckandClaude Opus 4.7 c1e4fb6b13 fix(f5tts): Ref-WAV Preprocessing — Loudness + Silence-Trim
F5-TTS reagiert empfindlich auf leise / verrauschte / zerhackte
Referenzen — wir haben bisher nur auf 24kHz mono + 10s geclipped.
Jetzt zusaetzlich:
  - silenceremove am Anfang (bis Speech einsetzt, <-50dB)
  - silenceremove am Ende (0.5s Stille nach letzter Speech = Cutoff)
  - loudnorm -16 LUFS (EBU R128) fuer konsistente Amplitude

Damit sieht das Modell saubere, konstant laute Referenz-Audios statt
kaputter Clips mit Ausklang oder leiser Aufnahme. Besonders bei Deutsch
(wo F5TTS_v1_Base schwach ist) hilft jede Input-Konsistenz der Quali.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:07:58 +02:00
duffyduckandClaude Opus 4.7 7dd13f1139 fix(f5tts): Referenz-WAV auf 10s clippen + txt neu transkribieren
F5-TTS hat ein Hard-Limit von 12s fuer das Referenz-Audio — laengere
WAVs werden intern abgeschnitten, aber unser ref_text war das komplette
Transkript. Text und Audio wurden dadurch unaligned, Render-Qualitaet
leidet und der initial Warmup-Render dauerte 57s statt 5s.

Fix:
  - normalize_ref_wav(max_seconds=10): ffmpeg schneidet auf 10s + 24kHz
    mono, gibt was_modified zurueck damit Caller den txt invalidieren kann
  - handle_voice_upload: clippt VOR der Transkription, Whisper sieht also
    nur die 10s → txt passt garantiert zum Audio
  - _do_tts: checkt vor jedem Render die WAV-Dauer. WAVs > 10.5s werden
    geclippt, .txt geloescht → on-the-fly Neu-Transkription beim Render

Bestehende kaputte Voices (wie MAIA mit 600+ Worten txt zu einem 20s
Audio) werden beim naechsten Render automatisch gefixt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:42:33 +02:00
duffyduckandClaude Opus 4.7 338793812e debug: Initial-Handshake Logs damit man sieht was passiert
Beim user kommt nach 'RVS verbunden' nichts mehr — Modell-Download
startet nicht, banner aktualisiert sich nicht. Vermutung: alter Code
laeuft noch (kein neu gebauter Container) ODER der Initial-Handshake
crashed silent (asyncio.create_task ohne await schluckt Exceptions).

- whisper + f5tts: Initial-Handshake mit logger.info Zeilen, damit
  man sieht ob er ueberhaupt ausgefuehrt wird
- f5tts: zusaetzlich exception-Catch + fehler-broadcast falls der
  Modell-Load crashed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:54:12 +02:00
duffyduckandClaude Opus 4.7 85c8d38ea3 fix: HF-Cache zurueck + Banner-Bug + config_request Pattern
Vier Bugs in einem Aufwasch:

1. HF-Cache als Bind-Mount zurueck
   xtts/hf-cache:/root/.cache/huggingface fuer beide Bridges. War vorher
   raus, dadurch jedes Container-Restart = ~3GB Whisper-Download +
   ~1GB F5-TTS-Download. User dachte 5min ist einmalig — ist aber bei
   jedem Restart. Jetzt: einmal pro Maschine geladen, fertig.

2. Banner zeigte stale "ready"
   whisper-bridge sendete beim Connect nur dann Status wenn Modell schon
   geladen war. Sonst blieb der App/Diagnostic Banner auf dem alten
   "ready" State von vor dem Restart haengen — User sah "bereit" obwohl
   gerade gar nichts geladen war. Jetzt wird IMMER ein Status broadcast:
   ready oder loading.

3. config_request Pattern
   aria-bridge wusste nicht wann Gamebox-Bridges sich (re)connecten.
   Wenn die nach aria-bridge kamen, verpassten sie den Config-Broadcast
   und blieben mit Hard-Defaults stehen.
   Jetzt: whisper- und f5tts-bridge senden beim Connect ein
   config_request, aria-bridge antwortet mit der persistierten Config
   (whisperModel, xttsVoice, f5tts*-Felder).

4. RVS ALLOWED_TYPES um config_request erweitert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:46:47 +02:00
duffyduckandClaude Opus 4.7 b35933b2b9 fix: dynamischer STT-Timeout — whisper Modell-Download nicht abkappen
aria-bridge horcht jetzt auf service_status fuer den Service 'whisper'.
Solange whisper-bridge im 'loading' steckt (Erst-Download large-v3 kann
1-2 Min dauern), gilt fuer stt_request ein Timeout von 300s statt 45s.
Sobald 'ready', zurueck auf 45s — reicht selbst fuer lange Audios.

Symptom vorher: Beim ersten Sprechen nach Container-Restart hat aria-
bridge nach 45s aufgegeben und lokal gefallback waehrend whisper-bridge
noch fleissig den Download laufen hatte. Damit wurde der Sinn der
Auslagerung kaputt gemacht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:06:04 +02:00
duffyduckandClaude Opus 4.7 6f4c35cae9 fix: WebSocket max_size hochgedreht — voice_upload sprengte Default 1MB
Symptom: aria-whisper-bridge bekam beim ersten internen stt_request
(via voice_upload mit WAV als base64, ~2.4MB) den Frame zu Gesicht,
default ws-max ist 1MB → mit Close-Code 1009 abgewiesen → Verbindung
tot → naechster stt_request lief in Timeout → lokales Fallback.

Fixes:
- whisper-bridge: max_size=50*1024*1024 in websockets.connect()
  (gleicher Wert wie f5tts-bridge schon hat)
- RVS-Server: maxPayload=50*1024*1024 in WebSocketServer-Optionen,
  damit der Server die Frames nicht selbst auf 1MB cappt bevor er
  sie an die Bridge weiterleitet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:54:36 +02:00
duffyduckandClaude Opus 4.7 62a1f9320b fix(whisper): kein eager preload mehr — wartet auf config-Broadcast
Vorher: Container-Start lud erst 'small' (env default), dann nochmal
das in Diagnostic konfigurierte Modell (z.B. large-v3) wenn die
config-Broadcast vom aria-bridge ankam. Doppelter Download, doppelte
Wartezeit, doppelter VRAM-Peak.

Jetzt:
- Initial wird NICHTS geladen
- aria-bridge sendet die persistierte voice_config.json kurz nach
  RVS-Connect → whisper-bridge sieht den richtigen Modellnamen
- config-Handler erkennt: noch nichts geladen ODER Wechsel
  → loading-Broadcast → ensure_loaded → ready-Broadcast
- stt_request-Handler: gleicher Status-Broadcast falls Race-Condition
  (Spracheingabe in den ersten 1-2s nach Container-Start)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:50:46 +02:00
duffyduck 43664edbb7 release: bump version to 0.0.5.6 2026-04-24 16:25:53 +02:00
duffyduckandClaude Opus 4.7 280cc29124 feat(app): service_status Banner oben in ChatScreen
App-Pendant zum Diagnostic-Banner. Wenn die Gamebox-Bridges (F5-TTS /
Whisper) ihren Lade-Status broadcasten, zeigt die App oben unter der
Verbindungs-Statusleiste ein farbiges Banner:
  Gelb  = irgendwas laedt   (NICHT wegtippbar)
  Gruen = alles bereit      (tippbar zum Schliessen)
  Rot   = Fehler

Banner aggregiert beide Services in einer Kachel. Dismiss-State wird
zurueckgesetzt sobald irgendein Service wieder in 'loading' geht
(z.B. Modell-Wechsel via Diagnostic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:24:47 +02:00
duffyduckandClaude Opus 4.7 a4746bfd07 feat: HF-Cache raus + service_status Banner in Diagnostic
Stefan akzeptiert die ~5min Modell-Download-Zeit nach jedem Container-
Start, dafuer keine 50GB Cache-Bloat mehr und kein Bind-Mount-Verzeichnis
zu pflegen.

- xtts/docker-compose.yml: hf-cache Bind-Mount entfernt fuer beide
  Bridges. Modelle werden im writable Container-Layer abgelegt und mit
  jedem `docker compose down` automatisch weggeraeumt.
- xtts/.gitignore: hf-cache/ Eintrag raus
- RVS ALLOWED_TYPES: service_status hinzu

Bridges broadcasten Lade-Status:
- f5tts-bridge: bei Connect 'loading' -> ensure_loaded -> 'ready'.
  Auch bei config-getriggertem Modell-Wechsel: erst 'loading' Broadcast,
  dann reload, dann 'ready'.
- whisper-bridge: gleiches Pattern. Modell wird jetzt erst nach
  RVS-Connect geladen damit der loading-Broadcast tatsaechlich rausgeht.

Diagnostic:
- server.js: service_status wird an Browser durchgereicht
- index.html: neues Banner unten rechts (fixed position) zeigt Status
  fuer beide Services. Aggregiert: Icon ist Lupe waehrend Loading,
  Check wenn alles ready, X bei Error.
- Wenn alles ready: X-Button erscheint (manuell schliessen) +
  nach 8s automatisches Fade-Out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:21:19 +02:00
duffyduckandClaude Opus 4.7 f638450c68 fix(android): minSdkVersion 23 -> 24 (Porcupine erfordert Android 7+)
@picovoice/porcupine-react-native deklariert minSdkVersion 24, dadurch
schlug der Manifest-Merger fehl wenn die App weiter auf 23 stand.
Android 7.0 ist eh das pragmatische Minimum (Geraete <7.0 sind <1% Markt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:08:10 +02:00
duffyduckandClaude Opus 4.7 1c647ab2a3 fix(deps): porcupine-react-native 3.0.6 existiert nicht — auf 3.0.5 pinnen
3.0.6 war geraten und gibt's nicht im npm Registry. Aktuelle stabile 3.x
ist 3.0.5; 4.0.0 hat Breaking Changes. Beide Picovoice-Packages auf
exakte Version gepinnt damit keine Auto-Bumps fies werden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:03:35 +02:00
duffyduckandClaude Opus 4.7 299959bfae release: bump version to 0.0.5.5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:02:37 +02:00
duffyduckandClaude Opus 4.7 f06b311c10 fix(cleanup-windows): ASCII-only damit Windows-PowerShell 5.1 parsen kann
Windows-PowerShell 5.1 liest .ps1 ohne UTF-8 BOM als Windows-1252;
em-dashes (-), Pfeile (->), Box-Zeichen, Emojis (OK/FAIL/WARN) wurden
als Mojibake interpretiert ("â€"" fuer "-") und sprengten den Parser.

Loesung: alle Sonderzeichen durch ASCII-Aequivalente ersetzt:
  - "-" / "->" statt em-dash / Pfeil
  - "===" statt Box-Linien
  - "[OK]" / "[FAIL]" / "[WARN]" statt Emojis
Funktioniert jetzt zuverlaessig auf jeder Windows-PS-Version ohne BOM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:01:56 +02:00
duffyduckandClaude Opus 4.7 962a977375 add: cleanup-windows Self-Elevation + .bat-Wrapper
cleanup-windows.ps1:
  - Defensive Set-ExecutionPolicy Bypass am Anfang
  - Self-Elevation: wenn nicht als Admin gestartet, relauncht das Script
    sich selbst als Admin mit -ExecutionPolicy Bypass + Original-Args.
    User muss nur einmal UAC bestaetigen, kein extra Befehl mehr noetig.

cleanup-windows.bat:
  - Wrapper der powershell.exe mit -ExecutionPolicy Bypass aufruft.
  - Funktioniert auch wenn Windows die .ps1 direkt blockt (z.B. unsignierte
    Scripts global gesperrt).
  - Aufruf: cleanup-windows.bat stefan [-SkipPrune] [-PruneOnly]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:59:55 +02:00
duffyduckandClaude Opus 4.7 9bfab12b28 add: cleanup-windows.ps1 — VHDX-Cleanup fuer Gamebox
Ein Script das auf der Gamebox (Windows + Docker Desktop + WSL2) alle
.vhdx Files findet und via diskpart compactet. Gibt den Speicherplatz
zurueck den man IN den Distros/Containern geloescht hat aber von der
VHDX bisher nicht freigegeben wurde.

Nutzung (PowerShell als ADMIN):
  .\cleanup-windows.ps1 stefan
  .\cleanup-windows.ps1 -User stefan -SkipPrune    # nur compacten
  .\cleanup-windows.ps1 -User stefan -PruneOnly    # nur prune

Default-Flow:
  1. docker system prune -a --volumes -f + builder prune
  2. wsl --shutdown
  3. Alle gefundenen ext4.vhdx (Docker Desktop + WSL-Distros) compacten
     via diskpart 'compact vdisk' (kein Hyper-V noetig)

Zeigt fuer jedes File "vor → nach (gespart X GB)" und am Ende eine
Gesamt-Summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:54:27 +02:00
duffyduckandClaude Opus 4.7 f94194edb4 fix: HF-Modell-Cache als Bind-Mount statt Docker Volume
Beide Bridges teilen sich jetzt einen Bind-Mount ./hf-cache:/root/.cache/
huggingface. Vorher waren das zwei getrennte Named Volumes
(f5tts-models + whisper-models), die unter Docker Desktop / Windows
in der docker-desktop-data.vhdx gelandet sind und die VHDX nie wieder
freigegeben haben — auch nach docker volume rm bleibt der belegte Platz
in der VHDX bis zum Factory Reset.

Bind-Mount loest beides:
  - Files direkt im xtts/hf-cache/ sichtbar, einfach im Explorer zu loeschen
  - Kein VHDX-Bloat mehr
  - Beide Container teilen sich den Cache (HF-Struktur identisch, keine
    Konflikte da andere Modelle)

Cleanup von vorhandenen 50GB:
  docker compose down
  docker volume rm xtts_f5tts-models xtts_whisper-models  (oder via
    Docker Desktop UI)
  Anschliessend in Docker Desktop: Settings -> Resources -> Disk image
    location -> Disk usage -> "Clean up" / Reset wenn die VHDX nicht
    schrumpft.

xtts/.gitignore: hf-cache/ + voices/ + .env

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:47:18 +02:00
duffyduckandClaude Opus 4.7 f87655db34 feat: F5-TTS Tuning ueber Diagnostic statt .env
Folgt der "keine neuen Settings in .env" Regel.

f5tts/bridge.py:
  - F5TTS_MODEL/CKPT_FILE/VOCAB_FILE/CFG_STRENGTH/NFE_STEP ENV-Vars raus
  - Hard-coded Defaults im Code (DEFAULT_F5TTS_*)
  - F5Runner besitzt Live-Settings als Instance-Vars + update_config()
  - config-Broadcast triggert Modell-Reload nur wenn Modell-relevantes
    sich aendert (cfg_strength/nfe_step ohne Reload)
  - F5TTS_DEVICE bleibt ENV (Hardware-Bootstrap)

xtts/docker-compose.yml: F5TTS_* ENV-Vars rausgenommen, Kommentar
verweist auf Diagnostic-Config.

aria-bridge: nimmt f5tts*-Felder im config-Handler entgegen, persistiert
sie in voice_config.json. Beim RVS-Connect broadcastet die Bridge die
persistierte Config einmalig — damit die f5tts-bridge nach Container-
Restart automatisch die zuletzt gewaehlten Settings bekommt, ohne dass
der User in Diagnostic was klicken muss.

Diagnostic UI:
  - Neuer aufklappbarer "F5-TTS Modell-Tuning (advanced)" Bereich
  - Felder: Modell-ID, Custom-Checkpoint, Vocab, cfg_strength, nfe_step
  - voice_config beim Laden: Felder werden zurueck in die UI gesetzt
  - sendVoiceConfig schickt die neuen Felder mit
  - Server: send_voice_config persistiert die Felder, leere Strings
    werden geloescht damit die Hard-Defaults greifen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:44:58 +02:00
duffyduckandClaude Opus 4.7 d73ea078d9 fix: F5-TTS Voice-Referenztext + Standard-Eintrag raus
Bug-Root: voice_upload schrieb "Das ist ein Referenz Audio." als Platzhalter
wenn die whisper-bridge nicht erreichbar war. F5-TTS bekam dann diesen Text
als Sprach-Anker, sah aber im WAV ganz andere Worte → verwirrtes Modell,
halluziniert in beliebiger Sprache (z.B. Spanisch).

Fixes:
- handle_voice_upload: schreibt KEINE Platzhalter-.txt mehr. Bei Failure
  bleibt die .txt weg → naechste TTS-Nutzung zieht via on-the-fly retry
  nach.
- _do_tts: Legacy-Platzhalter wird beim Render erkannt und geloescht,
  Transkription on-the-fly neu angezogen. Bestehende kaputte voices
  reparieren sich automatisch beim ersten Render.

UI-Aufraeumung: F5-TTS hat keine "Standard"-Stimme — der Eintrag ist raus
in App SettingsScreen + Diagnostic. Diagnostic-Dropdown hat jetzt einen
disabled-Hinweis "(keine Stimme gewaehlt)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:33:53 +02:00
duffyduckandClaude Opus 4.7 e1502fe3f3 fix(f5tts): cfg_strength hochgezogen damit Deutsch nicht ins Spanische rutscht
F5TTS_v1_Base ist hauptsaechlich auf Englisch+Chinesisch trainiert; bei
Deutsch (oder anderen Romance/Germanic-Sprachen) schwimmt der Generator
ohne starkes Conditioning gerne in eine andere Sprache.

- cfg_strength 2.0 → 2.5 (per ENV F5TTS_CFG_STRENGTH ueberschreibbar)
- nfe_step bleibt 32 (per ENV ueberschreibbar)
- F5TTS_CKPT_FILE / F5TTS_VOCAB_FILE als ENV — damit man eine Community-
  German-Checkpoint einhaengen kann ohne Code-Aenderung

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:30:08 +02:00
duffyduckandClaude Opus 4.7 1d10f5d94e feat: Porcupine Wake-Word Integration (Built-In Keywords, "Jarvis" default)
WakeWordService wrappt jetzt Picovoice Porcupine:
  - loadFromStorage(): Access Key + Keyword aus AsyncStorage, init Porcupine
  - configure(key, keyword): Settings-Wechsel, Re-Init
  - start(): wenn Porcupine bereit → 'armed' (passives Lauschen),
    sonst Fallback auf direktes 'conversing' (klassischer Modus)
  - onWakeDetected: Porcupine pausieren → 'conversing' → wakeCallback
  - endConversation: Porcupine wieder starten → 'armed' (Wake-Word weiter
    aktiv im Hintergrund, kein erneuter Tap noetig)
  - Pro Geraet eigene Wahl: jeder User kann sein eigenes Wake-Word haben

Settings: neuer Bereich "Wake-Word"
  - Picovoice Access Key Input (mit Eye-Toggle), kostenlos auf
    console.picovoice.ai
  - Built-In Keyword Chips: jarvis, computer, picovoice, porcupine,
    bumblebee, terminator, alexa, hey google, ok google, hey siri
  - "Speichern + Aktivieren" Button mit Status-Feedback
  - Hinweis dass "ARIA" Custom-Keyword spaeter via Diagnostic kommt

ChatScreen: ruft wakeWordService.loadFromStorage() beim Mount.

package.json: @picovoice/porcupine-react-native + react-native-voice-processor
hinzugefuegt — npm install + native rebuild noetig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:23:51 +02:00
duffyduckandClaude Opus 4.7 174e77f3ed feat: Conversation-Window — Gespraech endet nach Stille statt Endlos-Loop
Der Gespraechsmodus war bisher ein Endless-Loop: Mikro hat sich nach
jeder ARIA-Antwort wieder geoeffnet bis MAX_RECORDING_MS, danach Speech-
Gate verworfen und neu starten. Das Ohr blieb ewig an.

Neue Logik:
  audio.ts: startRecording(autoStop, noSpeechTimeoutMs?) — wenn der User
    innerhalb des Timeouts nicht anfaengt zu sprechen, wird Stille
    gemeldet → stopRecording → Speech-Gate verwirft → result=null.
  wakeword.ts: drei States off/armed/conversing. start() geht direkt in
    'conversing' (kein Wake-Word verfuegbar; Stub fuer spaetere Porcupine-
    Integration). endConversation() bei No-Speech.
  ChatScreen: Aufnahme bekommt das Window aus AsyncStorage durchgereicht.
    Bei null-Result → endConversation, UI-State synchron.
  Settings: neuer +/- Block "Konversations-Fenster" 3-20s (Default 8).

Mit dem Stub ist die Architektur bereit fuer Porcupine: dann geht
endConversation auf 'armed' statt 'off' und der Wake-Word-Detector
laeuft passiv weiter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:14:01 +02:00
duffyduckandClaude Opus 4.7 f4eb153f58 docs: README + issue.md auf Stand mit F5-TTS, Whisper-Gamebox, App-Settings
README:
- Architektur-Diagramm: Gamebox-Stack mit f5tts-bridge + whisper-bridge
- Voice Bridge: STT primaer remote (Gamebox), TTS via F5-TTS
- Diagnostic-Section: Voice-Status, Disk-Voll Banner, Auto-Transkription
- App-Features: VAD-Toleranz/Pre-Roll/Audio-Pause konfigurierbar
- XTTS-Section ersetzt durch "Gamebox-Stack — F5-TTS + Whisper"
- Roadmap Phase 1: alle juengsten Erledigungen ergaenzt

issue.md: alle erledigten Punkte der letzten Iterationen aufgenommen
(Pre-Roll, Decimal-TTS, voice_ready, Whisper-Gamebox, F5-TTS, AudioFocus
Pause, VAD-Setting, ...). Offene Liste auf den aktuellen Stand reduziert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:51:27 +02:00
duffyduck 76de8296b3 release: bump version to 0.0.5.4 2026-04-24 14:45:17 +02:00
duffyduckandClaude Opus 4.7 41cfd3c8a7 feat: Stille-Toleranz fuer Aufnahme einstellbar in App-Settings
Neuer +/- Block in SettingsScreen → Spracheingabe → "Stille-Toleranz",
1.0-8.0s, Default 2.8s. Wert in AsyncStorage (aria_vad_silence_sec).
audio.ts liest den Wert beim Aufnahme-Start und nutzt ihn fuer den
VAD-Auto-Stop-Schwellwert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:44:17 +02:00
duffyduck 675927ad35 release: bump version to 0.0.5.3 2026-04-24 14:41:59 +02:00
duffyduckandClaude Opus 4.7 0bd335b918 fix: TTS pausiert andere Apps statt zu ducken + VAD/Mic laenger
AudioFocus.requestDuck nutzt jetzt AUDIOFOCUS_GAIN_TRANSIENT (statt
TRANSIENT_MAY_DUCK) — Spotify/YouTube pausieren komplett solange ARIA
spricht und kommen nicht mitten drin wieder hoch.

PcmStreamPlayer.end() resolved jetzt erst wenn der native Writer-Thread
wirklich fertig ist (alle Samples aus dem Pre-Roll-Puffer ausgespielt).
audio.ts wartet entsprechend, bevor AudioFocus.release() gerufen wird —
behebt das "Musik dreht hoch waehrend Antwort noch laeuft"-Problem.

Mic-Aufnahme: VAD_SILENCE_DURATION_MS 1800 → 2800ms (mehr Toleranz fuer
Sprechpausen), MAX_RECORDING_MS 30s → 120s (laengere Erklaerungen
moeglich, Notbremse bleibt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:40:58 +02:00
duffyduckandClaude Opus 4.7 d57e91caf8 feat(phase2): XTTS durch F5-TTS ersetzt — Voice Cloning auf der Gamebox
Neuer aria-f5tts-bridge Container:
  - Python-Service, laedt F5TTS_v1_Base beim Start
  - Empfaengt xtts_request via RVS, synthetisiert mit Voice-Cloning,
    streamt PCM-Chunks (audio_pcm, 16-bit s16le) wie zuvor die XTTS-Bridge
  - Teilt lange Texte an Satzgrenzen, streamt satzweise
  - Fade-In auf erstem Chunk, Queue gegen parallel-Render

Voice-Management:
  - Speicherort weiterhin /voices/, aber jetzt als Paar
    {name}.wav + {name}.txt (F5-TTS braucht Referenz-Transkription)
  - voice_upload: WAV speichern, intern stt_request an whisper-bridge
    senden, Transkription als .txt ablegen → user muss nichts eintippen
  - On-the-fly Transkribierung: wenn eine WAV ohne .txt liegt, wird
    bei erstem Render/Preload nachgezogen
  - Bestehende RVS-Messages (voice_upload/xtts_list_voices/... etc.)
    bleiben unveraendert → keine App/Diagnostic-Aenderung noetig

Gaming-PC docker-compose:
  - xtts + xtts-bridge Services entfernt
  - f5tts-bridge + whisper-bridge bleiben/kommen rein
  - Volume xtts-models → f5tts-models

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:34:11 +02:00
duffyduckandClaude Opus 4.7 7e715b7a28 fix: _send_to_rvs gibt Success-Bool zurueck, _stt_remote bricht bei
Send-Fehler sofort ab statt in den 45s-Timeout zu laufen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:16:08 +02:00
duffyduckandClaude Opus 4.7 be30d22296 debug: Logs beim stt_request-Roundtrip — aria-bridge loggt beim Senden,
whisper-bridge loggt eingehende stt_request (id + Audio-Groesse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:13:41 +02:00
duffyduckandClaude Opus 4.7 19ce272873 fix(whisper): requests explizit als Dependency — faster-whisper 1.0.3
zieht sie selber nicht rein, Container crashed sonst beim Import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:59:59 +02:00
duffyduck 4882ebec33 release: bump version to 0.0.5.2 2026-04-24 13:50:19 +02:00
duffyduckandClaude Opus 4.7 9b1769269d feat(phase1): Whisper STT auf die Gamebox ausgelagert
Neuer Container aria-whisper-bridge auf der Gamebox — faster-whisper
CUDA mit float16. Der Container verbindet sich per WebSocket an den RVS,
nimmt stt_request entgegen, laeuft ffmpeg+Whisper, antwortet mit
stt_response. Hoert zusaetzlich auf config-Broadcasts und lädt das
Modell hot-swap bei Diagnostic-Wechsel.

aria-bridge ruft jetzt primaer die Gamebox an; nur wenn die nicht binnen
45s antwortet, faellt auf lokales Whisper (CPU) zurueck. Das lokale
Modell wird lazy geladen, spart RAM auf der VM.

RVS: stt_request/stt_response zur ALLOWED_TYPES-Liste.

Diagnostic-Voice-Config (whisperModel-Feld) bleibt unveraendert —
die Auswahl wird an die Gamebox durchgereicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:42:07 +02:00
duffyduck c2163b9064 release: bump version to 0.0.5.1 2026-04-23 22:02:17 +02:00
duffyduck 5729753b2e release: bump version to 0.0.5.0 2026-04-23 15:31:18 +02:00
duffyduckandClaude Opus 4.7 ba3a4eebb2 feat: voice_preload/voice_ready — Feedback wenn neue Stimme geladen ist
XTTS-Bridge:
  - empfaengt neuen voice_preload Type, rendert stumm "ja." fuer die Stimme
    via TTS-Queue (damit kein Konflikt mit echtem TTS)
  - horcht zusaetzlich auf config-Broadcasts: wenn Diagnostic global die
    Stimme wechselt, wird auto-preloaded
  - broadcastet voice_ready mit Dauer (loadMs) oder error

RVS: voice_preload + voice_ready zur ALLOWED_TYPES-Liste.

App (SettingsScreen): beim Wechsel senden wir voice_preload, zeigen einen
Spinner in der Voice-Row und einen Toast mit "Stimme X bereit (Ns)".
App (ChatScreen): Toast auch hier — falls User gerade nicht in Settings ist.

Diagnostic (server+UI): voice_ready wird an Browser durchgereicht, ein
Status-Text unter dem Voice-Dropdown zeigt "wird geladen" → "bereit".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:24:08 +02:00
duffyduck 351a7822e1 release: bump version to 0.0.4.9 2026-04-22 23:02:28 +02:00
duffyduckandClaude Opus 4.7 5e653969a6 fix: Voice-Auswahl funktioniert endlich + Diagnostic setzt alle Apps zurueck
XTTS-Bridge: im daswer123 local-Mode erwartet der Server speaker_wav als
Basename (z.B. "Maia"), nicht als Pfad. Wir haben bisher "/voices/Maia.wav"
geschickt, was der Server stumm verwirft und Default nimmt. Jetzt: speaker
name pur senden + Warnlog wenn File fehlt.

App: ChatScreen + SettingsScreen horchen auf type "config" vom RVS —
wenn in Diagnostic die globale XTTS-Voice gewechselt wird, werden alle
Apps auf den neuen Wert zurueckgesetzt (wie vom User gewuenscht).
Lokale App-Wahl bleibt sonst intakt und gewinnt pro Request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:32:40 +02:00
duffyduckandClaude Opus 4.7 3a17c5f08d feat: generisches Buchstabieren fuer unbekannte Akronyme
Nach der expliziten _UNIT_WORDS-Liste greift eine Fallback-Regel:
alle verbleibenden 2-5-Zeichen-Grossbuchstaben-Woerter werden
buchstabiert. XTTS → X T T S, USB → U S B, DNS → D N S, JSON → J S O N.

Spezielle Faelle (WLAN, NATO — als Wort gesprochen) koennen bei
Bedarf in _UNIT_WORDS explizit ueberschrieben werden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:17:04 +02:00
duffyduck ad7ec11be2 release: bump version to 0.0.4.8 2026-04-22 19:08:00 +02:00
duffyduckandClaude Opus 4.7 a014a05a37 feat: Pre-Roll-Buffer fuer TTS einstellbar in App-Settings
- Kotlin start() nimmt jetzt prerollSeconds als dritten Parameter
  (1.0-6.0s geclampt, Fallback 3.5s bei ungueltigem Wert)
- audio.ts liest Wert aus AsyncStorage vor jedem Stream-Start,
  exportiert Default/Min/Max/Key als Konstanten
- SettingsScreen: +/- Buttons direkt unter dem TTS-Toggle,
  Default auf 3.5s (von 2.5s) angehoben

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:06:55 +02:00
duffyduckandClaude Opus 4.7 d7e2f3f630 fix: Fade-In auf ersten PCM-Chunk — maskiert XTTS-Warmup-Glitches
XTTS daswer123 hat am Anfang jedes Renders Warmup-Artefakte — die
ersten autoregressiv generierten Tokens haben wenig Kontext und klingen
verzerrt. Ein 120ms Linear-Fade-In auf den ersten ausgehenden PCM-Chunk
blendet das sanft auf und versteckt die Glitches, ohne dass das echte
Audio danach leiser klingt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:01:36 +02:00
duffyduckandClaude Opus 4.7 0879a9713d fix: stream_chunk_size auf 250 erhoeht — weniger Render-Artefakte
XTTS daswer123 erzeugt an Chunk-Grenzen oft Glitches in den Worten
die ueber die Grenze gehen. 100 → 250 = weniger Grenzen pro Satz =
sauberere Sprachausgabe. Erste-Audio-Latenz steigt um ein paar Sekunden,
was aber OK ist seit die App Pre-Roll gepuffert ist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:56:00 +02:00
duffyduck d3b6612782 release: bump version to 0.0.4.7 2026-04-22 18:46:25 +02:00
duffyduckandClaude Opus 4.7 b26fe2e80c feat: Dezimalzahlen fuer TTS ausschreiben + Leading-Silence im Stream
- aria_bridge clean_text_for_tts: "0.1" / "0,5" / "1,25" wird jetzt als
  "null komma eins" / "null komma fuenf" / "eins komma zwei fuenf"
  ausgeschrieben. Lookahead verhindert Match auf IP-artige Strings.
- PcmStreamPlayer: 200ms Stille am Stream-Anfang, damit AudioTrack
  sauber anfaehrt und die ersten Worte nicht verschluckt werden.
  (XTTS-Warmup + play()-Startup-Latenz)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:44:38 +02:00
duffyduck 10e98338ed release: bump version to 0.0.4.6 2026-04-22 18:32:31 +02:00
duffyduckandClaude Opus 4.7 00338820c5 fix: Stream-Ende wartet auf playbackHeadPosition vor release()
AudioTrack.stop() + release() direkt nach dem letzten write() killt die
letzten Sekunden Audio — die Samples sind zwar im Buffer, aber noch
nicht durch die Hardware rausgespielt. Deshalb brach die Sprachausgabe
mitten im Satz ab (z.B. bei "diesmal").

Fix: Writer-Thread wartet im finally-Block bis playbackHeadPosition die
Anzahl geschriebener Frames erreicht, dann erst stop()/release().
Safety: 2s Stall-Detection, falls AudioTrack haengen bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:31:12 +02:00
duffyduck 6582eb0f90 release: bump version to 0.0.4.5 2026-04-22 18:18:20 +02:00
duffyduckandClaude Opus 4.7 80242de9bd feat: AudioTrack Pre-Roll — Playback startet erst nach 2.5s Vorrat
User-Diagnose: Erneutes Abspielen aus Cache funktioniert komplett, aber
Live-Stream bricht ab. Bedeutet: PCM kommt an, Cache ist okay — Problem
ist Buffer-Underrun im AudioTrack wenn XTTS (RTF 1.48 auf RTX 3060)
langsamer rendert als Echtzeit-Playback konsumiert.

Fix: AudioTrack.play() wird NICHT mehr sofort beim start() aufgerufen.
Stattdessen:
- start() baut AudioTrack, Writer-Thread startet, spielt aber noch nicht
- writeChunk() fuellt queue, Writer schreibt in AudioTrack-internen Buffer
  (blocked wenn der voll ist)
- Sobald bytesBuffered >= 2.5s Audio im Buffer: play() aufrufen
- Falls end() kommt bevor Pre-Roll erreicht (kurze Texte): trotzdem play()

Das gibt dem Stream Zeit Vorrat aufzubauen. XTTS kann dann pausieren
zwischen Text-Chunks ohne dass Playback stottert.

Pre-Roll 2.5s reicht fuer typische Render-Pausen zwischen Chunks.
Buffer groesse = 2x Pre-Roll damit wir auch extrem bursty Delivery
puffern koennen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:16:02 +02:00
duffyduckandClaude Opus 4.7 41ac3ab49a tune: stream_chunk_size 200 -> 100 gegen 6s Initial-Latenz
Mit RTF 1.48 (RTX 3060) rechnet XTTS fuer 200 chars ca. 6s bis erster
PCM-Chunk rauskommt — User wartet nach ARIA-Antwort 6s auf Sprachausgabe.

stream_chunk_size=100: Erster Chunk in ~3s bereit, reduziert
Initial-Latenz um ~50%. 100 chars sind auch noch gross genug dass
der AudioTrack-Buffer (128KB ≈ 2.7s Audio) zwischen Render-Chunks
nicht leerlaeuft → kein mid-sentence Abbruch wie bei 40.

Falls bei bestimmten Texten doch Gaps: stream_chunk_size zurueck auf
150, oder pre-roll im Android PcmStreamPlayer einbauen (nur starten
wenn X ms gepuffert sind).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:08:10 +02:00
duffyduckandClaude Opus 4.7 9e100f5a4d fix: stream_chunk_size 40 -> 200 gegen Audio-Abbrueche mid-sentence
Bei stream_chunk_size=40 teilte XTTS Text in ~40-char Batches.
Zwischen Batches pausiert XTTS (RTF 1.48 auf RTX 3060 → langsamer
als Realtime-Wiedergabe). AudioTrack-Buffer lief leer, Track
stoppte, nachkommender PCM kam zu spaet → Audio bricht mid-sentence
ab (User-Bug: bei 73-char Text Abbruch nach Wort 'diesmal' was genau
an der 40-char Grenze lag).

stream_chunk_size=200:
- Kurze Saetze (<200 chars) komplett in einem Render → kein Abbruch
- Laengere Texte: groessere Chunks, laenger Audio pro Chunk als
  Render-Pause → Buffer bleibt gefuellt
- Kompromiss: first-audio-latency etwas hoeher, aber keine Abbrueche

Wenn spaeter Audio-Abbrueche bei langen Texten: stream_chunk_size
noch groesser setzen ODER einen "pre-roll" Buffer in der App.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:06:25 +02:00
duffyduckandClaude Opus 4.7 e8994b84fd fix: XTTS local-Mode per ENV statt command-Override
Das Image-Default-CMD liest Konfig aus ENV Variablen:
  CMD: ... -ms \${MODEL_SOURCE:-"apiManual"}

Also reicht MODEL_SOURCE=local — command bleibt Image-Default und wir
sparen uns den brueckigen Override der schief ging (python nicht da,
flag-Namen raten, etc.).

Zusaetzlich: EXAMPLE_FOLDER=/voices damit der Speaker-Folder auf unser
gemountetes /voices zeigt (sonst /app/example was nur die Demo-Voices
enthaelt).

Kein command override mehr noetig — das Image macht alles wie vorher,
nur mit local-Mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:59:39 +02:00
duffyduckandClaude Opus 4.7 1c04882edf fix: XTTS command nutzt python3 statt python
Image hat nur /usr/bin/python3, kein 'python'-Symlink.
Vorher ging's weil kein command override — das Image-Default CMD
lief durch. Wir ueberschreiben nur damit wir -ms local setzen koennen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:58:29 +02:00
duffyduckandClaude Opus 4.7 ac6de75c2b fix: XTTS command braucht 'python -m xtts_api_server' als erstes Arg
NVIDIA-Entrypoint fuehrt 'exec \$@' aus — erstes Arg muss ein
ausfuehrbares sein. Nur Flags zu geben ('--listen') fuehrt zu
'exec: --: invalid option'.

Fix: command=['python','-m','xtts_api_server','-ms','local',...]
Damit wird der xtts_api_server Python-Modul gestartet und im
local-Mode konfiguriert.

Ob die Flag-Namen exakt stimmen (-hs/-p/-ms/-o/-mf/-sf) — falls
nicht, poppt ein klarer Python-Fehler im Log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:49:29 +02:00
duffyduckandClaude Opus 4.7 a61ce9ea95 feat: XTTS auf local-Mode (dauerhaft im VRAM) + /tts_stream + Fallback
Root cause der langen Render-Zeiten und /tts_stream 400-Errors:
daswer123 default ist apiManual/api-Mode — Modell wird pro Request
gefetched/reloaded, Streaming unsupported.

Fix in xtts/docker-compose.yml:
  command: ['--listen', '-p', '8020', '-t', 'http://0.0.0.0:8020',
            '-ms', 'local',
            '-o', '/app/output', '-mf', '/app/xtts_models', '-sf', '/voices']

-ms local:
  - Modell dauerhaft im GPU-VRAM (~2GB, passt auf RTX 3060 mit 12GB)
  - Render startet sofort, kein per-Request-Load mehr
  - /tts_stream unterstuetzt → echtes progressive streaming
  - time-to-first-audio ~500ms statt 8-11s

xtts/bridge.js:
  /tts_stream primary, /tts_to_audio/ als Fallback wenn Stream fehlt.
  Robust: wenn User spaeter den Mode wieder umstellt, fallback greift.

Erste Ladung nach dem Wechsel dauert einmalig laenger (Modell ins VRAM
laden). Danach: schnell + streaming.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:38:53 +02:00
duffyduckandClaude Opus 4.7 eb36bd43ce fix: XTTS nutzt direkt /tts_to_audio/ — /tts_stream nicht verfuegbar
XTTS-Server (daswer123) im API-Modus antwortet auf /tts_stream mit:
  HTTP 400: "HTTP Streaming is only supported for local models"

Das Feature braucht MODE=local in der XTTS-Config (Modell direkt im
Server-Prozess). Userbetreibt im Remote-Modus → kein Streaming.

Der try /tts_stream + fallback /tts_to_audio Ansatz war reine Ver-
schwendung: jeder Request wartete 6ms auf 400, bevor der Fallback
griff. Jetzt geht's direkt an /tts_to_audio/.

Kein echtes Streaming, aber:
- Queue sorgt fuer sequentielle Verarbeitung (kein Overlap mehr)
- 32x AudioTrack-Buffer faengt den bursty Response ab
- aria-bridge spiegelt audio_pcm nicht mehr (kein Doppel-Audio)

Wenn User spaeter /tts_stream haben will:
  XTTS-Server mit MODE=local oder --streaming-mode starten,
  dann kann man /tts_stream als primary einfuehren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:23:55 +02:00
duffyduckandClaude Opus 4.7 c5f983b530 fix: /tts_stream — speaker_wav muss IMMER als query-param gesetzt sein
XTTS-Server (daswer123) markiert speaker_wav als required Pydantic-Feld.
Mein 'if (speakerWav) qs.set(...)' hat den Key bei default-voice
weggelassen → HTTP 422 'Field required, input: null' → Fallback auf
/tts_to_audio/ hat gegriffen, aber Streaming nie gefunden.

Log-Beweis vom User:
  XTTS /tts_stream 422: {"detail":[{"type":"missing","loc":["query",
    "speaker_wav"],"msg":"Field required","input":null}]}

Fix: Key immer setzen, leerer String bei default-voice. POST-Variante
(/tts_to_audio/ JSON-Body) hat das auch so akzeptiert — GET-Query nun
gleiches Verhalten.

Ab jetzt sollte /tts_stream endlich greifen und echte Streaming-Latenz
(~300-500ms) zeigen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:47:28 +02:00
duffyduckandClaude Opus 4.7 4e8df5efa1 fix: XTTS-Endpoint mit Fallback-Chain + Diagnose-Logs
Problem: /tts_stream hat bei User nicht funktioniert → keine
Sprachausgabe mehr. Server hatte vorher 405 fuer POST geantwortet,
meine Umstellung auf GET scheint aber einen anderen Fehler zu
produzieren der nicht geloggt wurde.

Fix:
- streamXTTSAsPCM() = /tts_stream (GET, Streaming) mit ausfuehrlichem
  Error-Logging bei non-200 Response
- streamXTTSBatch() = /tts_to_audio/ (POST, Batch) als Fallback
- handleTTSRequest versucht Stream zuerst, bei Exception Fallback
  auf Batch — so gibt's IMMER Audio, auch wenn /tts_stream kaputt ist
- Log zeigt welcher Pfad benutzt wurde

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:53:10 +02:00
duffyduckandClaude Opus 4.7 022a691a51 fix: /tts_stream als GET mit Query-Params (war 405 Method Not Allowed)
daswer123 xtts-api-server hat /tts_stream nur als GET:
  allow: GET → POST gab 405 → Request hing.

Umstellung:
- method: 'GET'
- text/language/speaker_wav/stream_chunk_size als URLSearchParams
  im Query-String
- kein body mehr (kein req.write, kein Content-Length)

Ab jetzt echter streaming-Flow: Samples kommen waehrend XTTS noch
rendert, time-to-first-audio ~300-500ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:52:16 +02:00
duffyduckandClaude Opus 4.7 8e27378e81 revert: XTTS-Endpoint zurueck auf /tts_to_audio/
/tts_stream war bei der aktiven daswer123-Version nicht erreichbar —
Requests hingen stille, App bekam kein Audio.

Zurueck auf /tts_to_audio/ + Queue + 32x AudioTrack-Buffer. Das ist
zwar nicht echt-streaming aber stabil. Ueberlappung sollte durch die
Queue weg sein, Buffer toleriert den bursty Delivery.

Echt-Streaming-Migration spaeter mit verifizierter Server-Version
oder anderem Endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:48:27 +02:00
duffyduck 85a2ead28e release: bump version to 0.0.4.4 2026-04-20 16:42:19 +02:00
duffyduckandClaude Opus 4.7 2e7f5c175a fix: Streaming TTS nutzt jetzt echt den /tts_stream Endpoint von XTTS
Vorher: /tts_to_audio/ — XTTS rendert kompletten WAV BEVOR es
antwortet. Mein "streaming" war nur fake-chunking des fertigen WAV.
Time-to-first-audio = komplette Render-Zeit (2-4s), dann Burst,
dann Stille. Plus bei langen Antworten: Queue blockiert.

Jetzt: /tts_stream — daswer123's chunked-transfer endpoint.
Samples flutschen waehrend der Generierung durch die Response raus.

Parameter:
- stream_chunk_size=40 → XTTS rendert in ~40-char Haeppchen intern,
  time-to-first-audio ~300-500ms statt 2-4s
- WAV-Header kommt wie gewohnt am Anfang (44 Bytes), danach raw PCM
  → mein existierender Header-Parser + 8KB-Chunker passen weiter

Voraussetzung: daswer123/xtts-api-server hat diesen Endpoint (ab
Version ~0.8.x). Sollte bei der aktuellen Version drin sein.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:35:55 +02:00
duffyduck 6486f197ac release: bump version to 0.0.4.3 2026-04-20 08:01:46 +02:00
duffyduckandClaude Opus 4.7 af915effb9 fix: Disk-Banner — Safe-Cleanup als Default + Aggressiv hinter Expander
Safe-Variante (Default):
  docker builder prune -a -f && docker image prune -a -f
  → Build-Cache + ungenutzte Images, KEINE Volumes angefasst.
  → 90% des Platzproblems geloest, Null Datenverlust-Risiko.

Aggressive Variante (nur auf Wunsch, hinter 'Mehr'-Button):
  docker system prune -a --volumes -f
  → Zusaetzlich ungenutzte Volumes.
  → Nur sicher wenn alle ARIA-Container LAUFEN (sonst werden
     openclaw-config/claude-config/aria-shared als "ungenutzt"
     behandelt und zerstoert — Sessions weg).
  → Hinweistext orange hervorgehoben mit Warnung.

Banner-Button 'Sicher aufraeumen' kopiert die sichere Variante.
'Mehr' klappt die Erklaerung der aggressiven Variante aus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:39:22 +02:00
duffyduckandClaude Opus 4.7 5768f7fef5 feat: Disk-Space Banner im Diagnostic mit Cleanup-Command zum Kopieren
Server:
- checkDiskSpace() prueft alle 30s 'df -B1 /shared' (zeigt Host-Disk
  da /shared ein Volume auf dem Docker-FS ist)
- 4 Stufen: ok (<70%), info (70%), warn (85%), critical (95%)
- Broadcastet disk_status nur bei Aenderung (Level oder Prozent)
- currentDiskStatus wird gecached → neu verbundene Clients bekommen
  den aktuellen Stand sofort beim 'init'

UI:
- Sticky Banner ganz oben, versteckt wenn Disk ok
- Farbe nach Level: gelb (info), orange (warn), rot (critical)
- Zeigt Prozent, Used/Total/Avail in GB, konkrete Situation
- Cleanup-Command als monospace Code mit Copy-Button ('docker system
  prune -a --volumes -f') — Click auf Code oder Button kopiert ins
  Clipboard, Fallback auf Range-Selektion
- 'Schliessen' Button fuer temporaeres Ausblenden (kommt aber wieder
  bei naechster Aenderung)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:36:36 +02:00
duffyduckandClaude Opus 4.7 497d68688e fix: Streaming TTS — Queue in XTTS-Bridge + groesserer Android-Buffer
1) Ueberlappende Streams
   Wenn zwei xtts_requests schnell hintereinander kamen, rannten
   sie parallel durch handleTTSRequest. Beide HTTP-Requests an XTTS
   liefen gleichzeitig, beide streamen PCM an App → Chunks aus BEIDEN
   Renders landeten interleaved in der AudioTrack-Queue → Chaos.

   Fix: ttsQueue als Promise-Chain — handleTTSRequest() haengt sich
   ans Ende der Kette an. Requests werden sequenziell abgearbeitet.

2) AudioTrack-Buffer zu klein fuer bursty Delivery
   XTTS /tts_to_audio/ ist NICHT echt streaming — der Server rendert
   intern den kompletten WAV und schickt ihn dann burst-weise. Der
   alte 8x-MinBuffer (ca 200-400ms) war zu klein um das abzufangen.

   Fix: Buffer auf 32x MinSize / mind. 128KB = ca. 2.7s bei 24kHz.
   Das toleriert typische XTTS-Render-Latenz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:27:16 +02:00
duffyduckandClaude Opus 4.7 ec1b14614f fix: Streaming TTS — doppeltes Audio + Gaps zwischen Saetzen
Zwei Probleme gefunden:

1) DOPPELTES AUDIO (Kern-Ursache der Artefakte)
   aria-bridge hat audio_pcm von XTTS-Bridge empfangen und per
   _send_to_rvs rebroadcastet. RVS broadcast geht an ALLE Clients
   ausser Sender — die App bekam jeden Chunk also zwei mal:
     XTTS-Bridge → RVS → App + aria-bridge
     aria-bridge → RVS → App (nochmal!) + XTTS-Bridge
   Zwei ueberlagerte PCM-Streams klingen wie Doubled/Artefakte.
   Fix: aria-bridge ignoriert audio_pcm jetzt. messageId schickt
   XTTS-Bridge selbst im Payload (via xtts_request -> messageId).

2) GAPS ZWISCHEN SAETZEN (abgehackt)
   xtts/bridge.js teilte Text in ~150-char Chunks und rief pro Chunk
   einen eigenen /tts_to_audio/ Request. Zwischen Chunks lag die
   XTTS-Render-Zeit (1-3s) → hoerbare Pausen.
   Fix: cleanText geht JETZT in einem Request komplett an XTTS.
   Ein zusammenhaengender Stream → keine Satz-Gaps mehr.
   Kompromiss: Erste Samples kommen spaeter (ganze Text-Render dauert
   laenger als der erste Satz alleine), aber dann kontinuierlich
   ohne Unterbrechung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:15:57 +02:00
duffyduck 6a9bdd6ed2 release: bump version to 0.0.4.2 2026-04-19 23:01:55 +02:00
duffyduckandClaude Opus 4.7 1f690d23f9 feat: Mode-Wechsel auch aus Diagnostic global syncronisiert
Vorher: Diagnostic's setMode sendete einen faked chat mit der
Aktivierungsphrase ('ARIA, Hangar-Modus') — das wurde erst in
_process_core_response auf dem ARIA-Antwort-Text detected, war
unzuverlaessig und nutzte nicht den sauberen mode-Message-Path.

Nachher: sauberer set_mode-Pfad mit Live-Sync.

diagnostic/server.js:
- Neue action 'set_mode' → sendet type=mode an RVS direkt
- RVS-Message-Handler: type=mode Broadcast von Bridge wird an
  Browser-Clients durchgereicht

diagnostic/index.html:
- setMode() nutzt jetzt action=set_mode (keine Phrase mehr)
- updateModeUI separat — wird bei Broadcast auch aufgerufen
- Mode-Broadcast vom Server syncs UI live (andere Diagnostic/App
  hat gewechselt → unser UI aktualisiert sofort)
- Button data-mode + MODE_LABELS auf kanonische IDs umgestellt
  (nicht_stoeren, fluester statt dnd, whisper)

bridge/modes.py:
- canonical_id() liefert die IDs die App + Diagnostic kennen
  (nicht_stoeren, fluester, ...) — damit Broadcast-ID zur UI-ID passt

bridge/aria_bridge.py:
- _broadcast_current_mode nutzt canonical_id statt enum.name.lower()

Flow jetzt:
  Diagnostic wechselt Mode → set_mode → Bridge → persist + broadcast
  → alle Apps + alle Diagnostic-Browser-Tabs aktualisieren sofort

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:57:41 +02:00
duffyduckandClaude Opus 4.7 04e77e5269 feat: Betriebsmodus global + persistent + Live-Sync
Vorher:
- Modus war nur in-memory in der Bridge, Restart = zurueck auf NORMAL
- App-Wechsel wurde zwar empfangen, aber nicht an andere Geraete
  gebroadcastet (nur Bestaetigung an den Sender)
- Neue App-Verbindung wusste nicht welcher Modus gerade aktiv ist

Jetzt:
- Persistiert in /shared/config/mode.json beim Wechsel
- Beim Bridge-Start: _load_persisted_mode() holt letzten aktiven Modus
- _broadcast_current_mode() sendet an ALLE Clients (Broadcast) —
  jedes verbundene Geraet bekommt live den Wechsel mit
- Bei RVS-Reconnect: sofortiger Broadcast damit neu verbundene Apps/
  Diagnostic ihre UI syncen koennen
- Loop-Schutz: payload.sender=="bridge" wird im mode-Handler ignoriert
  (sonst echo → Broadcast-Storm bei verbundenem RVS)

Beispiel-Flow:
  Geraet A aktiviert 'Hangar'
  → Bridge empfaengt mode-msg
  → persist in mode.json
  → broadcast an alle Clients (mit sender="bridge")
  → Geraet B/C/Diagnostic empfangen → UI updated sofort
  → Bridge-Restart spaeter: HANGAR wird wieder geladen

Anmerkung zu echten OS-Push bei geschlossener App:
Das braucht FCM/Firebase + BackgroundService — deutlich mehr Arbeit,
ist separat als Feature fuer spaeter zu sehen. Live-Sync bei geoeffneter
App (WebSocket verbunden) funktioniert jetzt zuverlaessig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:53:47 +02:00
duffyduckandClaude Opus 4.7 d147424e27 fix: Modus-Wechsel via App/ID funktioniert jetzt
Bug: App ModeSelector sendet rvs.send('mode', { mode: 'normal' })
mit ID, Bridge's detect_mode_switch() sucht aber nach Aktivierungs-
phrasen wie 'aria, normal-modus' → kein Match → Modus-Wechsel
wurde ignoriert, TTS-Verhalten blieb auf NORMAL haengen.

Fix:
- modes.py: mode_from_id() mappt IDs zu Mode-Enum
  ('normal', 'dnd', 'nicht_stoeren', 'fluester', 'whisper',
   'hangar', 'gaming' — flexibel)
- aria_bridge.py: mode-Handler versucht erst ID-Mapping, dann
  Phrasen-Erkennung als Fallback
- Unbekannte Modi werden geloggt
- Bestaetigung wird an alle Clients zurueckgesendet damit App-UI
  synchron bleibt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:51:18 +02:00
duffyduckandClaude Opus 4.7 592922ce6e feat: App XTTS-Voice-Auswahl + Aufnahme + Loeschen (geraetelokal)
App Settings: Voice-Sektion (nur wenn TTS an)
- Liste aller XTTS-Server-Stimmen mit Auswahl-Radio + X zum Loeschen
- 'Standard' fuer Diagnostic-Default-Voice (keine lokale Ueberschreibung)
- 'Aktualisieren' Button laedt Liste neu (xtts_list_voices via RVS)
- 'Eigene Stimme aufnehmen' oeffnet VoiceCloneModal

VoiceCloneModal: 30s Aufnahme + Upload
- Vorlese-Text (>30s Lesedauer, thematisch passend)
- Rot-pulsierender Stop-Button, live Timer + Progressbar
- Auto-Stop bei 30s, Hinweise ab 15s ('genug fuer gute Clonung')
- Nach Stop: Namenseingabe (a-Z, 0-9, _, -), Upload via voice_upload
- Nach Upload: Modal schliesst, Settings bekommt xtts_voice_saved
  und setzt automatisch die neue Stimme als gewaehlt

Voice-Flow App → Bridge → XTTS (geraetelokal):
- Jeder chat/audio/tts_request schickt aria_xtts_voice (AsyncStorage)
  mit der Message mit
- Bridge speichert _next_voice_override bei chat/audio Empfang,
  nutzt es fuer die naechste ARIA-Antwort und resettet dann
- Fallback: globale xtts_voice aus voice_config.json (Diagnostic)

Ergebnis:
- Gerat A hat 'stefan' geclont → ARIA antwortet Geraet A mit stefan
- Gerat B hat nichts gewaehlt → ARIA antwortet Geraet B mit Default
- Diagnostic-Einstellung wirkt als fallback-default fuer neue Geraete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:48:24 +02:00
duffyduckandClaude Opus 4.7 f5db0870f1 fix/feat: XTTS-Voice korrekt persistiert, Loeschen + Voice-per-Request
Bug-Fix: Voice-Auswahl verschwand nach Page-Load
- xtts_voices_list Handler rebuildet das Dropdown — vorheriger select.value
  ging dabei verloren. Jetzt wird der Wert gemerkt und nach Rebuild
  wiederhergestellt (falls die Stimme noch existiert).

Feature: Stimmen loeschen (Diagnostic)
- XTTS-Bridge: neuer handleDeleteVoice — entfernt /voices/<name>.wav
  und schickt aktualisierte Liste per xtts_voices_list
- RVS: xtts_delete_voice in ALLOWED_TYPES
- Diagnostic Server: Action xtts_delete_voice forwarded via RVS
- Diagnostic UI: renderVoiceList zeigt alle Custom-Voices mit X-Button
  Bei Loeschen der gerade aktiven Stimme: auf Default zuruecksetzen

Feature: Voice-per-Request in Bridge
- App kann mit jedem Chat ein voice-Feld mitschicken
- Bridge merkt sich _next_voice_override, nutzt es fuer die NAECHSTE
  ARIA-Antwort (einmalig, dann reset)
- tts_request (Play-Button) akzeptiert voice im Payload als Override
- Fallback: globale xtts_voice aus voice_config.json
- So kann jedes Geraet seine eigene Stimme haben ohne den globalen
  Default zu aendern

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:43:26 +02:00
duffyduckandClaude Opus 4.7 8e3ebfcc28 feat: App TTS-Einstellungen vereinfacht + Mund-Button fuer lokales Muten
SettingsScreen:
- Piper-Reste entfernt (defaultVoice, highlightVoice, Speed-Slider,
  Highlight-Trigger-Info)
- Nur noch EIN Toggle 'Sprachausgabe auf diesem Geraet' — geraetelokal,
  persistent in aria_tts_enabled (AsyncStorage)
- Keine Config-Propagation mehr via RVS (das waere ja global gewesen)
- Hinweis dass Stimme + Voice-Cloning zentral in der Diagnose sind

ChatScreen: Mund-Button (👄 / 🤐)
- Neben Ohr-Button im Eingabebereich, NUR sichtbar wenn TTS im Setting
  grundsaetzlich aktiv ist
- Tap toggelt Mute: 👄 an / 🤐 rot gemutet
- Persistent in aria_tts_muted (AsyncStorage)
- Stoppt bei Muten sofort laufende Wiedergabe (stopPlayback)
- Settings-Toggle wird alle 2s gepollt damit Aenderungen greifen
  (einfache Loesung ohne globalen State-Context)

Audio-Handling respektiert lokalen Zustand
- Incoming audio/audio_pcm: nur abspielen wenn ttsDeviceEnabled && !ttsMuted
- Cache wird TROTZDEM immer geschrieben — Play-Button funktioniert
  spaeter aus Cache, auch waehrend Mute
- audioService.handlePcmChunk akzeptiert silent-Flag: skipt AudioTrack
  aber baut weiterhin den WAV-Cache pro messageId

Jedes Android-Geraet mit der App hat seinen eigenen Mute-Zustand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:33:36 +02:00
duffyduckandClaude Opus 4.7 17ce4e11e0 feat: Piper komplett entfernt — nur noch XTTS v2 als TTS
Breaking Change: wenn XTTS-Bridge (Gaming-PC) offline ist, bleibt ARIA
stumm. Chat-Antworten kommen weiter an, aber kein Audio. Das ist
bewusst akzeptiert — XTTS klingt einfach grauenhaft viel besser.

Bridge (aria_bridge.py):
- from piper import ... raus
- VoiceEngine-Klasse komplett entfernt (synthesize, speak, select_voice)
- EPIC_TRIGGERS + load_epic_triggers raus (Highlight-Voice-Feature
  ohne Piper sinnlos)
- self.voice_engine, voice_name, requested_voice Aufrufe weg
- _process_core_response: immer XTTS, kein Fallback
- tts_request Handler: immer XTTS
- config Handler: nur ttsEnabled + xttsVoice + whisperModel
- import wave raus

bridge/requirements.txt: piper-tts raus
bridge/Dockerfile: Kommentar aktualisiert
docker-compose.yml: ./aria-data/voices Mount raus
aria-data/config/aria.env.example: PIPER_RAMONA/PIPER_THORSTEN raus
get-voices.sh: komplett geloescht (war nur Piper-Downloader)

Diagnostic UI (index.html):
- Piper Panel (Standard-Stimme / Highlight-Stimme / Speed-Sliders) weg
- TTS Engine Dropdown weg (immer XTTS)
- TTS Diagnose Tab zeigt nur noch XTTS-Status + Test-Button
- sendVoiceConfig sendet nur noch ttsEnabled/xttsVoice/whisperModel
- toggleXTTSPanel als no-op Legacy-Stub (JS-Calls bleiben safe)

Diagnostic Server (server.js):
- handleSendVoiceConfig: nur noch ttsEnabled + xttsVoice + whisperModel
- handleTestTTS: via xtts_request (nicht mehr Piper subprocess)
- handleCheckTTS: via xtts_list_voices ueber RVS
- handleGetVoiceConfig/Defaults bereinigt
- Highlight-Trigger UI bleibt, wird aber von Bridge nicht mehr
  ausgewertet (dead-code im UI, spaeter ggf. fuer XTTS-Voice-Switch)

README + issue.md aktualisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:24:42 +02:00
duffyduckandClaude Opus 4.7 d598fa7faf feat: Streaming TTS — PCM-Stream statt WAV-Chunks (Weg A)
Pipeline: XTTS-Server → xtts-bridge → aria-bridge → RVS → App AudioTrack

XTTS-Bridge (Gaming-PC):
- streamXTTSAsPCM(): liest /tts_to_audio/ Response inkrementell,
  parst WAV-Header (samplerate/channels), teilt PCM in 8KB-Chunks
  (~170ms bei 24kHz s16 mono) und sendet jeden als audio_pcm.
- Finaler Chunk mit final=true nach letztem Text-Chunk

aria-bridge:
- audio_pcm Handler leitet payload 1:1 weiter, filled messageId aus
  requestId → messageId Map falls XTTS-Bridge messageId nicht hatte
- Alter xtts_response Pfad bleibt als Legacy-Fallback (WAV)

RVS: audio_pcm in ALLOWED_TYPES

Android Native:
- PcmStreamPlayerModule (Kotlin): AudioTrack MODE_STREAM mit
  Writer-Thread und BlockingQueue. start(rate, ch) / writeChunk(b64)
  / end() / stop()
- 8x MinBufferSize grosszuegig dimensioniert, glatt auch bei
  Netz-Aussetzern
- Registered im MainApplication via PcmStreamPlayerPackage

App JS:
- audioService.handlePcmChunk(): erkennt neue Session (messageId-Wechsel),
  started nativen Stream, cached PCM-Bytes pro Message. Bei final=true
  Stream sauber schliessen + _savePcmBufferAsWav → WAV-File im
  tts_cache/<messageId>.wav
- _savePcmBufferAsWav: baut 44-byte WAV-Header (PCM s16le, korrekte
  samplerate/channels), haengt alle gesammelten base64-PCM-Chunks an
- stopPlayback beendet auch aktiven PCM-Stream
- ChatScreen routet type=audio_pcm an handlePcmChunk, bei final
  setzt audioPath in der Message

Play-Button: falls messageId einen audioPath hat → WAV aus Cache
(Sound-basiert), egal ob Original-TTS Piper oder XTTS war.

Audio-Focus:
- requestDuck() beim Stream-Start, release() bei Stream-Ende
- Andere Apps (Spotify etc.) werden leiser waehrend ARIA spricht

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:01:27 +02:00
duffyduckandClaude Opus 4.7 7958cc2629 feat: TTS-Zeitbereiche + Diagnostic-Debug-Toggle + Play-Button respektiert Engine
TTS-Cleanup erweitert:
- Zeitbereiche: '8:00-9:00 Uhr' / '8-9 Uhr' → 'acht bis neun Uhr'
- Uhrzeiten: '8:30 Uhr' → 'acht Uhr dreissig', '15 Uhr' → 'fuenfzehn Uhr'
- Kleine Zahlen-Bereiche: '5-6' → 'fuenf bis sechs' (nur ≤24)
- Zahlen 0-59 als deutsche Woerter (inkl. 'einundzwanzig', 'fuenfundvierzig')

Diagnostic: TTS-Debug Einblenden
- Checkbox 'TTS-Text einblenden' in der Chat-Test Kopfzeile
- Unter ARIA-Nachrichten erscheint die aufbereitete Variante
  (blauer Border + Label 'TTS:')
- Nur in Diagnostic, nicht in der App
- LocalStorage persistiert den Toggle-Zustand
- Minimaler JS-Port von clean_text_for_tts als Fallback

Play-Button respektiert Engine:
- Bridge: tts_request nutzt jetzt die aktive TTS-Engine (Piper/XTTS),
  Text wird durch clean_text_for_tts aufbereitet
- messageId wird vom Play-Button mitgeschickt → Bridge verknuepft
  generiertes Audio mit der urspruenglichen Message
- XTTS-Chunks: requestId → messageId Map (LRU 100 Eintraege),
  beim xtts_response wird die Basis-UUID extrahiert und die
  messageId dem audio-Frame angehaengt
- App cached auch XTTS-Audio jetzt (letzter Satz pro Message —
  echte Chunk-Konkatenation bleibt TODO)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:48:32 +02:00
duffyduck d507022894 release: bump version to 0.0.4.1 2026-04-19 21:00:49 +02:00
duffyduckandClaude Opus 4.7 d7a775f4ae fix: QR-Code overflowed Container — auf SVG umgestellt mit width/height 100%
Der QR wurde mit createImgTag() als fester Pixel-IMG gerendert und
ueberlappte den Warnhinweis + Button rechts daneben. Fix:

- createSvgTag mit cellSize=4 + scalable=true
- SVG skaliert auf width:100%/height:100% der 220x220 Box
- Container: flex-shrink:0 (damit Flex ihn nicht weiter schrumpft)
- overflow:hidden als Sicherheit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:28:41 +02:00
duffyduckandClaude Opus 4.7 f862fd65ed fix: Runtime-Config Layout + Eye-Toggle fuer Token-Felder
- Eingabefelder haben jetzt width:100% + box-sizing:border-box,
  keine Ueberlappung mehr im Grid
- Token-Felder haben einen Augen-Button daneben (👁/👀) zum
  Anzeigen/Verbergen des Inhalts
- Kleineres Label-Grid (140px statt 150px), grosszuegigerer Gap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:27:56 +02:00
duffyduckandClaude Opus 4.7 2077b4f876 feat: Runtime-Config via Diagnostic UI — kein .env-Sync mehr
Framework fuer zentrale Runtime-Konfiguration:
- /api/runtime-config (GET/POST) persistiert in /shared/config/runtime.json
- Werte haben Vorrang ueber die ENV-Variablen aus aria.env
- Feldliste: RVS_HOST/PORT/TLS/TOKEN, ARIA_AUTH_TOKEN, WHISPER_MODEL/LANGUAGE
- Atomic write (tmp + rename) fuer Konsistenz

Bridge:
- load_config() liest nach aria.env noch runtime.json und ueberschreibt
  die Werte. Aenderungen werden beim Neustart der Bridge uebernommen.

Diagnostic UI:
- Neue Sektion "Runtime-Konfiguration" in Einstellungen
- Formular fuer RVS-Credentials + Aria-Auth-Token
- "Speichern" persistiert, triggert auch QR-Code-Regenerierung
- Hinweis: Diagnostic-Container selbst bleibt auf ENV (erstmal)

issue.md konsolidiert — 6 groessere Tasks dieser Session als erledigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:18:37 +02:00
duffyduckandClaude Opus 4.7 2a94e9320e feat: QR-Code Onboarding + TTS-Audio-Cache im Filesystem
QR-Code Onboarding
- Diagnostic: GET /api/onboarding gibt RVS-Credentials zurueck
- Einstellungen-UI: neue Sektion mit QR-Code (qrcode-generator via CDN)
- Format kompatibel mit bestehendem QRScanner.parseQRData (host/port/tls/token)
- App-SettingsScreen hatte QR-Scanner bereits — funktioniert out of the box
- Warnhinweis zu Token im Klartext

TTS-Audio-Cache
- Bridge: jede ARIA-Chat-Nachricht bekommt eine messageId (UUID)
  Audio-Payload wird mit messageId verknuepft (Piper-Pfade)
- ChatScreen: messageId + audioPath in ChatMessage Interface
- audioService.cacheAudio(): speichert Base64 in DocumentDirectory/tts_cache/<id>.wav
- audioService.playFromPath(): spielt aus Cache ohne Regenerierung
- Play-Button: wenn audioPath gesetzt → aus Cache, sonst tts_request
- cleanupOldTTSCache(): alte unreferenzierte WAVs (>30 Tage) weg
- Persistiert via AsyncStorage — ueberlebt App-Restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:16:25 +02:00
duffyduckandClaude Opus 4.7 aed38c2c97 feat: NO_REPLY-Filter + Audio-Ducking + TTS-Cleanup
1) NO_REPLY Token wird in Bridge und Diagnostic erkannt und still
   verworfen. Toleranz fuer Variationen (Whitespace, Punkt, Quotes).
   Kein Chat-Eintrag, kein TTS.

2) AudioFocusModule (Kotlin) mit requestDuck / requestExclusive /
   release. AudioService ruft:
   - requestExclusive() bei Aufnahme-Start → andere Apps pausieren
   - requestDuck() bei TTS-Playback-Start → andere Apps leiser
   - release() bei Stop/Queue-Ende
   MainApplication registriert AudioFocusPackage.

3) clean_text_for_tts() in Bridge — zentrale Aufbereitung:
   - <voice>...</voice> Tag wird bevorzugt (falls ARIA es schreibt)
   - Code-Bloecke (``` und `) komplett raus
   - Markdown (Fett/Kursiv/Links/Headings/Listen) geschleift
   - Einheiten ausgeschrieben: 22GB → 22 Gigabyte, 85% → 85 Prozent
   - Abkuerzungen buchstabiert: CPU → C P U, API → A P I
   - URLs durch "ein Link" ersetzt
   Genutzt in VoiceEngine.synthesize und im XTTS-Request — Chat-Text
   an die App bleibt unveraendert (original Markdown), nur TTS kriegt
   die aufbereitete Version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:10:54 +02:00
duffyduckandClaude Opus 4.7 7726544ead docs: Neue Issues - TTS-Cleanup, Audio-Ducking, Config-UI, NO_REPLY-Bug
- Bug: NO_REPLY wird als "NO" angezeigt
- Audio-Ducking + Mute waehrend Aufnahme
- Spracheingabe-Timeout erhoehen
- TTS-Text-Aufbereitung (Code raus, Einheiten ausschreiben)
- Audio-Cache in Messages (kein Regenerieren beim Play-Button)
- Piper evtl. entfernen
- .env → Diagnostic UI migrieren
- QR-Code Onboarding
- XTTS Web-Oberflaeche / zentral via Diagnostic
- Root-Cause OpenClaw Session-Reset pruefen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:04:47 +02:00
duffyduckandClaude Opus 4.7 bd88d0cb0d fix: Reset-File-Regex - Timestamp endet mit Z (ohne Punkt davor)
Die OpenClaw Reset-Files heissen <uuid>.jsonl.reset.<iso>Z
(nicht <uuid>.jsonl.reset.<iso>.Z). Der falsche Regex matchte
nie, alle Archive wurden als "verwaist" angezeigt statt als "archiv".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:32:24 +02:00
duffyduck 42d37084d7 release: bump version to 0.0.4.0 2026-04-18 12:28:08 +02:00
duffyduckandClaude Opus 4.7 8e28a0c774 feat: Archivierte Session-Versionen (OpenClaw .reset.* Files) in Diagnostic
OpenClaw resettet Sessions beim ersten chat.send nach Container-Restart
(wenn abortedLastRun / systemSent Inkonsistenz erkannt wurde) und
benennt die alte .jsonl in .jsonl.reset.<timestamp>.Z um. Der Inhalt
war also gar nicht verloren, nur unsichtbar.

Diagnostic:
- handleListSessions scannt jetzt auch *.jsonl.reset.* Files
- Reset-Files bekommen archived:true + resetAt-Timestamp
- Neue UI-Sektion "Archivierte Versionen" (collapsible <details>)
  mit Export-Button, zeigt aufklappbar alle gesicherten alten Sessions
- Aktivieren ist fuer Archive deaktiviert (zerstoert aktive Session)
- Loeschen + Export stehen zur Verfuegung

tools/export-jsonl-to-md.js:
- Standalone Node-Script zum Konvertieren beliebiger .jsonl (auch reset-Files)
- Nutzbar via stdin, exakt gleiche Export-Logik wie Diagnostic
- Fuer Rettungsaktionen direkt auf der VM

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:24:54 +02:00
duffyduckandClaude Opus 4.7 74f571d3a9 fix: Session Msgs-Counter zaehlt echte Nachrichten, nicht alle Zeilen
Vorher: wc -l auf der .jsonl — zaehlt auch Tool-Calls, Run-Events,
Metadata-Eintraege mit. Diagnostic zeigte z.B. "10 Msgs" fuer eine
Session mit 6 echten User/Assistant-Nachrichten.

Jetzt: grep -cE '"role":"(user|assistant)"' — zaehlt nur echte
Konversations-Messages. Matcht wie der Export und die Chat-History
das interpretieren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:09:01 +02:00
duffyduckandClaude Opus 4.7 f33d016696 fix: Gespraechsmodus - strenger Speech-Gate + Crash-Prevention
Probleme:
- Hintergrundgeraeusche wurden als Sprache erkannt und an Whisper geschickt
- App stuerzte nach laengerem Zuhoeren ab (OOM / Cache-Ueberlauf)

Aenderungen:
- VAD_SPEECH_THRESHOLD_DB -35 -> -28 (filtert Raum-Ambient)
- VAD_SPEECH_MIN_MS 300 -> 500 (keine Huestler/Klopfer mehr)
- Max-Aufnahmedauer 30s (Notbremse gegen Runaway-Loops)
- _cleanupStaleCacheFiles(): alte aria_recording_/aria_tts_ Files (>30s)
  werden vor jeder neuen Aufnahme geloescht
- ChatScreen: capMessages() begrenzt Messages-Array auf 500 Eintraege
  (OOM-Schutz in langen Gespraechen)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:05:15 +02:00
duffyduckandClaude Opus 4.7 17e94b3348 fix: Thinking indicator beim Seitenladen nicht mehr sichtbar
Zwei display:-Deklarationen im inline-style der Diagnostic-Chat-Leiste
haben sich gegenseitig ueberschrieben — 'display:flex' war die zweite
und hat 'display:none' aushebelt. Indicator war so beim Seitenaufbau
sichtbar bis JS ein idle-Event empfing.

- HTML: 'display:flex' aus inline-style entfernt
- JS: beim Anzeigen explizit display='flex' setzen (statt 'block')

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:58:39 +02:00
duffyduck 6a6504073e release: bump version to 0.0.3.9 2026-04-18 11:52:53 +02:00
duffyduckandClaude Opus 4.7 57e1955c7b fix: Thinking indicator respringt nach chat:final durch trailing events
Nach chat:final kommen oft noch agent-Events rein (Core raeumt nach),
die den Thinking-Indicator wieder anspringen liessen.

- Diagnostic: 3s-Settled-Window nach chat:final, agent_activity-Broadcasts
  werden in dem Fenster unterdrueckt (idle kommt weiter durch).
- Bridge: Gleiches Fenster in _emit_activity() — App bekommt keine
  trailing thinking/tool-Events mehr nach dem finalen Antwort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:51:22 +02:00
duffyduckandClaude Opus 4.7 c1ca2f1c62 docs: cleanup.sh + README updates for latest features
- cleanup.sh: sicherer (default) + aggressiver (--full) Docker-Cleanup
  mit Speicher-Report vor/nach
- README: Phase-1-Liste, Diagnostic-Features und App-Features um die
  neuen Punkte ergaenzt (Speech Gate, Session-Persistenz, Session-Export,
  App Thinking-Indicator, Whisper-Modellauswahl, 16kHz-Aufnahme)
- README: Neuer Abschnitt "Docker-Cleanup" mit cleanup.sh Usage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:46:12 +02:00
duffyduck 745fb4f267 release: bump version to 0.0.3.8 2026-04-18 11:41:12 +02:00
duffyduckandClaude Opus 4.7 009aed0a18 feat: Whisper model selector + 16kHz mono recording
- App: AudioSamplingRateAndroid 16000 + AudioChannelsAndroid 1
  → Whisper bekommt direkt sein Ziel-Format, kein Resample mehr
- Bridge: STTEngine.reload() laedt Modell zur Laufzeit neu
  (tiny/base/small/medium/large-v3)
- Bridge: Config-Message triggert Hot-Reload wenn whisperModel sich aendert
- Bridge: Default auf 'medium' (besser als 'small' bei aehnlicher Latenz)
- Diagnostic: Neue Sektion "Whisper (Spracherkennung)" mit Dropdown,
  auto-save bei Auswahl, beim Laden wird der gespeicherte Wert gesetzt
- Diagnostic/Server: send_voice_config merged whisperModel in voice_config.json
- aria.env.example: WHISPER_MODEL + WHISPER_LANGUAGE dokumentiert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:37:27 +02:00
duffyduckandClaude Opus 4.7 f59c5f1170 feat: Thinking indicator + cancel button in the app
- Bridge: _emit_activity() spiegelt OpenClaw agent events als agent_activity
  an RVS, dedupliziert State-Wechsel. chat:final/error senden idle.
- Bridge: Neuer cancel_request-Handler ruft Diagnostic /api/cancel per HTTP.
- Diagnostic: Neuer POST /api/cancel Endpoint (gleiche Logik wie WS-Cancel).
- RVS: agent_activity + cancel_request in ALLOWED_TYPES.
- App: Gelber Indicator ueber der Input-Bar mit Text je nach Activity,
  roter Abbrechen-Button. Cancel sendet cancel_request via RVS.
- issue.md: Erledigte Bugfixes + Features konsolidiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:22:02 +02:00
duffyduckandClaude Opus 4.7 f003c29b7a feat: Session export as markdown in Diagnostic
- ⬇ Button per Session-Zeile — exportiert auch inaktive Sessions
- Server parst JSONL, extrahiert User/Assistant-Nachrichten mit Timestamp
- Metadata-Prefix wird entfernt, Markdown mit # Session-Header generiert
- Browser-Download via Blob + download-Attribut

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:14:15 +02:00
duffyduckandClaude Opus 4.7 ae5ef4ff83 fix: Stuck "ARIA denkt..." indicator after pipeline ends
- pipelineEnd() now broadcasts agent_activity: idle unconditionally
- chat:error and chat:final paths broadcast idle outside of active pipeline
- Gateway close event ends active pipeline + broadcasts idle
- Prevents indicator from hanging after timeout/error/disconnect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:11:12 +02:00
duffyduckandClaude Opus 4.7 e8dd14e1a8 fix: Session persistence - respect user choice across container restarts
- sessionFromFile flag prevents auto-pick after first start
- Atomic write (temp + rename) with loud error logging
- Auto-pick filters out aria-bridge/aria-diagnostic when user sessions exist
- handleSetActiveSession reports persistence failures to client

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:03:26 +02:00
duffyduckandClaude Opus 4.6 5086091f52 fix: Speech gate - only send recording if actual speech detected
- VAD_SPEECH_THRESHOLD_DB = -35 (louder than silence threshold)
- Needs 300ms of speech before counting as real speech
- Recording discarded if only background noise detected
- Prevents sending garbage to Whisper in conversation mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:20:05 +02:00
duffyduck ed4a0c3ea4 release: bump version to 0.0.3.7 2026-04-11 13:18:17 +02:00
duffyduckandClaude Opus 4.6 465a3d8741 fix: Conversation mode starts recording immediately when ear button tapped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:15:26 +02:00
duffyduckandClaude Opus 4.6 6572637e8f docs: update README with conversation mode, multi-attachments, markdown cleanup
- Conversation mode (ear button) documented in App Features
- Multiple attachments + paste support
- Markdown cleanup for TTS
- Auto-Update FileProvider + check button
- Roadmap: 22 items in Phase 1 completed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:43:09 +02:00
duffyduckandClaude Opus 4.6 4bc02da982 docs: update issue.md - move completed items, clean up open list
28 items completed, 10 remaining open

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:23:04 +02:00
duffyduck db5e36f62c release: bump version to 0.0.3.6 2026-04-11 12:19:00 +02:00
duffyduckandClaude Opus 4.6 65c30e008a fix: Inverted FlatList - newest messages always visible at bottom
- No more scrollToEnd/scrollToIndex needed
- FlatList inverted=true with reversed data
- New messages appear at bottom automatically
- User scrolls up to see history (natural chat behavior)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:17:32 +02:00
duffyduck 51a97340fe release: bump version to 0.0.3.5 2026-04-11 12:13:41 +02:00
duffyduckandClaude Opus 4.6 81af73bc1d fix: Use scrollToIndex with viewPosition:1 for reliable bottom scroll
- scrollToIndex targets last message at bottom of viewport
- onScrollToIndexFailed fallback to scrollToEnd
- More reliable than scrollToEnd with dynamic heights

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:12:24 +02:00
duffyduck f92896af95 release: bump version to 0.0.3.4 2026-04-11 12:09:23 +02:00
duffyduckandClaude Opus 4.6 7365845cdd fix: Auto-scroll retry with multiple delays (100, 300, 600, 1000ms)
FlatList needs time to render - single setTimeout(150) was unreliable.
Now tries 4 times on initial load, 2 times for new messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:07:54 +02:00
duffyduck e3d6b6c47e release: bump version to 0.0.3.3 2026-04-11 12:04:37 +02:00
duffyduckandClaude Opus 4.6 8d0322c0c8 fix: Remove duplicate closure ending in ChatScreen (build error)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:03:20 +02:00
duffyduckandClaude Opus 4.6 9332520ef1 fix: Comprehensive markdown/formatting cleanup for TTS (Piper + XTTS)
- Remove **bold**, *italic*, `code`, code blocks, links, headers, quotes, lists
- Replace newlines with natural pauses (period/comma)
- Remove quotation marks, empty brackets
- Fixes text being swallowed/garbled by TTS engines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:47:04 +02:00
duffyduckandClaude Opus 4.6 797256cc5b docs: mark conversation mode as done, keep Porcupine as Phase 2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:42:33 +02:00
duffyduckandClaude Opus 4.6 89dbb5b893 feat: Conversation mode (ear button) - auto-record after ARIA speaks
- Ear button activates conversation mode (green dot)
- After TTS playback finishes → 800ms pause → auto-start recording
- VAD stops recording on silence → sends to ARIA → ARIA answers → TTS → loop
- Like a natural conversation / walkie-talkie mode
- Audio service fires onPlaybackFinished when queue empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:40:55 +02:00
duffyduckandClaude Opus 4.6 3758cbc098 docs: mark scroll bugs as fixed in issue.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:37:53 +02:00
duffyduckandClaude Opus 4.6 5380586fd4 fix: Auto-scroll to last message on app start + new messages
- useEffect on messages array instead of onContentSizeChange
- Instant jump (no animation) when loading history
- Animated scroll for single new messages
- Scroll pauses when user scrolls up, resumes at bottom

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:37:30 +02:00
duffyduckandClaude Opus 4.6 c1f444257a feat: Multiple attachments + paste support (App + Diagnostic)
App:
- Multiple pending attachments (horizontal scroll preview)
- Individual remove (X) or clear all
- Send button shows when any attachment pending
- All files sent before text message

Diagnostic:
- Clip icon for file selection (multiple)
- Paste images/files from clipboard (Ctrl+V)
- Pending preview with thumbnails
- Files sent via RVS before text message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:34:33 +02:00
duffyduckandClaude Opus 4.6 acea6f84cb docs: mark attachment text feature as done in issue.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:06:13 +02:00
duffyduckandClaude Opus 4.6 cece7c6920 feat: Attachments not sent immediately - add text/voice before sending
- File/photo selection stores as pending (not sent immediately)
- Preview bar shows pending attachment above input field
- User can add text message before sending (e.g. "Was siehst du?")
- Send button appears when attachment is pending (even without text)
- Placeholder changes to "Text zum Anhang (optional)..."
- X button to cancel pending attachment
- File + text sent together (file first, then chat message)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:05:50 +02:00
duffyduckandClaude Opus 4.6 7150519c31 docs: mark text message bug as fixed in issue.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:59:48 +02:00
duffyduckandClaude Opus 4.6 b5c94a6d12 docs: mark auto-update bugs as fixed in issue.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:54:57 +02:00
duffyduck dde1d40de4 release: bump version to 0.0.3.2 2026-04-11 09:53:54 +02:00
duffyduckandClaude Opus 4.6 9b74919f8d fix: release.sh clears Metro + Gradle cache before build (version consistency)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:51:41 +02:00
duffyduck f03889c213 release: bump version to 0.0.3.1 2026-04-10 23:19:40 +02:00
duffyduckandClaude Opus 4.6 3b398c20d2 fix: Bridge chat handler was missing send_to_core (text messages ignored)
The chat handler checked sender but never forwarded the text to aria-core.
Only voice messages worked because they went through the audio→STT→send_to_core path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:13:29 +02:00
duffyduck f9d6e2354a release: bump version to 0.0.3.0 2026-04-10 09:07:20 +02:00
duffyduck 1bdaa0ee39 release: bump version to 0.0.2.9 2026-04-10 09:01:47 +02:00
duffyduckandClaude Opus 4.6 9d222e51f2 fix: release.sh deletes old APKs on RVS before uploading new one
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:00:45 +02:00
duffyduckandClaude Opus 4.6 c8e17ca5d3 fix: Auto-update APK install via FileProvider + dynamic version
- Native ApkInstallerModule: FileProvider content:// URI for Android 7+
- REQUEST_INSTALL_PACKAGES permission in AndroidManifest
- file_paths.xml for FileProvider cache access
- APP_VERSION reads from package.json (not hardcoded)
- "Auf Updates pruefen" button in Settings
- Version display reads from package.json dynamically

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:59:52 +02:00
duffyduckandClaude Opus 4.6 f3ea0753cf docs: add auto-update FileProvider bug + update check button to issue.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:55:30 +02:00
duffyduck fcd3709229 release: bump version to 0.0.2.8 2026-04-10 08:49:47 +02:00
duffyduckandClaude Opus 4.6 7075f1cf4c docs: document .env.example with detailed comments, explain both tokens in README
- ARIA_AUTH_TOKEN: Gateway auth (who can talk to ARIA)
- RVS_TOKEN: Pairing token (same room in RVS relay)
- RVS_UPDATE_HOST: SSH target for auto-update APK copy
- All variables with German comments and examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:45:26 +02:00
duffyduckandClaude Opus 4.6 efdfa6ffcf docs: add auto-update APK not reaching RVS bug to issue.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:39:59 +02:00
duffyduckandClaude Opus 4.6 eb8dfbc54b fix: Proxy SSH volume read-write (ARIA can manage keys without -F workaround)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:34:35 +02:00
duffyduckandClaude Opus 4.6 0e2392ea06 docs: add 6 new bugs/features to issue.md
- Session persistence on container restart
- App: text/image/attachment messages not working (only voice)
- App: audio stops randomly
- App: auto-scroll to last message on start + new messages
- App: add text/voice to attachments
- Prioritized bugs section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:31:49 +02:00
duffyduckandClaude Opus 4.6 1b92af2ecc fix: XTTS chunk size 150 chars (faster render, preload overlaps playback)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:52:56 +02:00
duffyduckandClaude Opus 4.6 21e07ac3eb fix: XTTS streaming mode - send each chunk immediately, comma between sentences
- Back to streaming: render chunk → send immediately → next chunk
- App plays with preloading queue (no waiting for all chunks)
- Comma instead of dot between sentences in chunk (no "Punkt" read aloud)
- Sentence-ending dots already removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:48:50 +02:00
duffyduckandClaude Opus 4.6 cac9d28736 fix: XTTS splits concatenated audio into ~8s parts (seamless with preload)
- All chunks rendered and PCM concatenated (consistent voice)
- Split into ~8 second WAV parts (not per-sentence)
- 8s is long enough for preload overlap, small enough for WebSocket
- Parts include part/totalParts metadata

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:41:14 +02:00
duffyduckandClaude Opus 4.6 a9ad19ed14 feat: XTTS concatenates chunks into seamless WAV (no stuttering)
- All chunks rendered sequentially, PCM data concatenated
- Single WAV with proper header sent back (no queue needed in app)
- If total > 800KB, split into parts (WebSocket limit)
- Eliminates stuttering between sentences

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:40:16 +02:00
duffyduckandClaude Opus 4.6 bc87f86e4a docs: update issue.md with XTTS streaming as next priority
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:38:21 +02:00
duffyduck e5ed2898d2 release: bump version to 0.0.2.7 2026-04-10 02:24:58 +02:00
duffyduckandClaude Opus 4.6 232053d4ec fix: XTTS groups sentences into ~250 char chunks for consistent voice quality
- 2-3 sentences per chunk (more context = stable voice/volume)
- Max 250 chars per chunk (keeps WebSocket packets manageable)
- Dots re-added between sentences within a chunk (natural pauses)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:23:29 +02:00
duffyduckandClaude Opus 4.6 a98842bd1f fix: Audio preloading to prevent stuttering, remove trailing dots for XTTS
- Preload next audio while current plays (eliminates gap between sentences)
- Remove trailing dots from sentences (XTTS reads them aloud)
- stopPlayback cleans up preloaded audio

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:21:19 +02:00
duffyduck 42d0ce8c4b release: bump version to 0.0.2.6 2026-04-10 02:11:04 +02:00
duffyduckandClaude Opus 4.6 34994e532f fix: Audio queue for sequential TTS playback (no overlap/skip)
- Audio packets queued instead of stopping previous
- _playNext() plays sequentially, each sentence after the previous
- stopPlayback() clears queue
- Fixes overlapping/skipping with XTTS sentence-by-sentence rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:09:35 +02:00
duffyduckandClaude Opus 4.6 53e85853e2 fix: XTTS bridge splits text into sentences sequentially
- XTTS-Bridge does sentence splitting (not ARIA-Bridge)
- Sequential rendering: correct order guaranteed
- Each sentence sent as separate xtts_response
- Markdown removal before splitting
- App starts playback after first sentence (faster UX)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:03:29 +02:00
duffyduckandClaude Opus 4.6 f820264a20 fix: XTTS splits long text into sentences before sending (WebSocket size limit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:56:25 +02:00
duffyduckandClaude Opus 4.6 94e5e4b665 docs: update README with XTTS v2 setup details, voice cloning guide
- Architecture diagram for XTTS flow (Gaming-PC ↔ RVS ↔ ARIA-VM)
- Port 8020 (not 8000), token must match, model caching
- Voice cloning step-by-step guide
- TTS engine switching (Piper/XTTS) with fallback
- Known limitation: RVS zombie connections

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:49:08 +02:00
duffyduckandClaude Opus 4.6 6a1661f781 fix: XTTS bridge shares /voices volume with XTTS server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:40:41 +02:00
duffyduckandClaude Opus 4.6 bf001cf304 fix: XTTS voice dropdown shows saved voice after page reload
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:34:00 +02:00
duffyduckandClaude Opus 4.6 92f555a038 fix: XTTS voices list + upload use fresh RVS connection with response wait
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:24:55 +02:00
duffyduckandClaude Opus 4.6 e86db3aceb fix: voice upload uses send() via server, not client-side sendToRVS_raw
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:15:29 +02:00
duffyduckandClaude Opus 4.6 dee88aec98 fix: voice upload Base64 conversion (chunked, no stack overflow)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:08:32 +02:00
duffyduckandClaude Opus 4.6 3db2ea9635 fix: Bridge _send_to_rvs ping-check before send, force reconnect on zombie
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:37:22 +02:00
duffyduckandClaude Opus 4.6 d2148353e6 fix: TTS toggle global above engine selector, health check /docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:27:55 +02:00
duffyduckandClaude Opus 4.6 8fa91dcd97 fix: XTTS model cache volume path /app/xtts_models
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:44:29 +02:00
duffyduckandClaude Opus 4.6 4d9ed81c10 fix: XTTS bridge port 8020, longer startup wait
- XTTS API runs on port 8020 (not 8000)
- Bridge waits up to 5min for model download (30x10s)
- Health check uses / instead of /docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:39:45 +02:00
duffyduck 3ace2dc22c updated image public path 2026-04-07 23:06:26 +02:00
duffyduck 37200803ce release: bump version to 0.0.2.5 2026-04-05 20:12:56 +02:00
duffyduck 2295516836 release: bump version to 0.0.2.4 2026-04-05 19:51:19 +02:00
duffyduckandClaude Opus 4.6 b5a31d3a4a docs: update README with XTTS, auto-update, watchdog, TTS settings
- Architecture: Added XTTS v2 (Gaming-PC) and auto-update flow
- Diagnostic: Thinking indicator, cancel button, TTS tab, voice cloning
- App: Play button, chat search, auto-update, voice speed settings
- RVS: Auto-update APK distribution over WebSocket
- Watchdog: 2min warning → 5min doctor --fix → 8min container restart
- Roadmap: Phase 1 fully completed, updated Phase 2+3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:46:16 +02:00
duffyduckandClaude Opus 4.6 404cb1d483 feat: XTTS v2 integration, auto-update system, TTS engine abstraction
- XTTS v2: Docker setup for Gaming-PC (GPU), bridge via RVS relay
- XTTS: Voice cloning UI in Diagnostic (multi-file upload)
- XTTS: Engine selectable (Piper local vs XTTS remote) with fallback
- Auto-Update: RVS serves APK over WebSocket (no HTTP needed)
- Auto-Update: App checks version on start, prompts install
- Auto-Update: release.sh copies APK to RVS via scp
- Bridge: TTS engine abstraction (piper/xtts), config persistent
- Bridge: xtts_response handler, tts_request on-demand
- Diagnostic: TTS engine dropdown, XTTS voice panel, voice cloning
- App: Play button on ARIA messages, chat search, update service
- Wake word: Disabled LiveAudioStream (crash fix), Phase 1 placeholder
- Watchdog: Container restart after 8min stuck
- Chat backup: on-the-fly to /shared/config/chat_backup.jsonl

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 19:42:10 +02:00
21 changed files with 37 additions and 2422 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ ARIA_AUTH_TOKEN=change-me-to-a-long-random-string
# App, Bridge, Diagnostic und XTTS-Bridge verbinden sich hierueber.
# Alle muessen den gleichen Host, Port und Token nutzen.
# Hostname des RVS-Servers (z.B. rvs.example.de oder mobil.hacker-net.de)
# Hostname des RVS-Servers (z.B. rvs.example.de oder example.com)
RVS_HOST=rvs.example.de
# Port auf dem der RVS laeuft (muss mit rvs/docker-compose.yml uebereinstimmen)
+2 -6
View File
@@ -242,8 +242,7 @@ Danach wird der Proxy gepatcht:
| `aria-data/ssh/` | SSH-Key fuer den Zugriff auf aria-wohnung (Brain + Proxy teilen den Key) |
| `aria-data/brain/qdrant/` | Vector-DB-Storage (Bind-Mount, gitignored) |
| `aria-data/brain/data/` | Skills, Embedding-Modell-Cache (Bind-Mount, gitignored) |
| `aria-data/brain-import/` | **Drop-Folder** fuer Markdown-Saatgut. Inhalt komplett gitignored ausser `.gitkeep` + `README.md`. Stefan kippt MDs rein wenn er was migrieren will, klickt Diagnostic-„Migration aus brain-import/" — sonst leer. DB ist Truth, brain-import nur Cold-Start-Schleuse |
| `.claude/aria-vm.env` | **Lokal pro Dev-Maschine** — wie erreicht die Workstation die VM (IP/Hostname). Gitignored, `.example` als Vorlage. Wird genutzt fuer direktes `curl` gegen die Brain-API von ausserhalb der VM |
| `aria-data/brain-import/` | `AGENT.md`, `USER.md.example`, `TOOLING.md.example` — Quelle fuer den initialen Memory-Import in die Vector-DB |
| `aria-data/config/diag-state/` | Diagnostic State (z.B. zuletzt aktive Session) |
### /shared/config/ (im aria-shared Volume)
@@ -317,7 +316,7 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
### Tabs
- **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 + Filter, Edit/Add/Delete, Konversation-Status mit Destillat-Trigger, **Token/Call-Metrics mit Subscription-Quota-Tracking**, Bootstrap & Migration (3 Wiederherstellungs-Wege), Gehirn-Export/Import (tar.gz). Info-Buttons () ueberall mit Modal-Erklaerung.
- **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.
- **Dateien**: Browser fuer `/shared/uploads/` mit Multi-Select + "Alle markieren" + Bulk-Download (ZIP bei 2+) + Bulk-Delete. Live-Update der Chat-Bubbles beim Delete.
@@ -356,7 +355,6 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
- **Voice-Ready Toast**: Beim Wechsel zeigt die App "Stimme X bereit (X.Ys)" sobald der Preload durch ist
- **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
- **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
- **Mehrere Anhaenge**: Bilder + Dateien sammeln, Text hinzufuegen, dann zusammen senden
- **Paste-Support**: Bilder aus Zwischenablage einfuegen (Diagnostic)
- **Anhaenge**: Bridge speichert in Shared Volume, ARIA kann darauf zugreifen, Re-Download ueber RVS
@@ -869,8 +867,6 @@ docker exec aria-brain curl localhost:8080/memory/stats
- [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] **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] **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] 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-Suche mit Next/Prev Navigation statt Filter
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10302
versionName "0.1.3.2"
versionCode 10208
versionName "0.1.2.8"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.1.3.2",
"version": "0.1.2.8",
"private": true,
"scripts": {
"android": "react-native run-android",
-237
View File
@@ -1,237 +0,0 @@
/**
* Memory-Browser — Liste mit Suche + Filter, Tap oeffnet MemoryDetailModal.
*
* Eingesetzt von:
* - SettingsScreen → Sektion "Gedächtnis" (kompletter Editor)
* - Inbox-Modal (Notizen-Button neben Lupe) — kann aber auch Bubbles
* aus dem Chat als zusaetzlichen Filter zeigen
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
Alert,
Modal,
} from 'react-native';
import brainApi, { Memory } from '../services/brainApi';
import MemoryDetailModal from './MemoryDetailModal';
const TYPE_LABELS: Record<string, string> = {
identity: 'Identität', rule: 'Regeln', preference: 'Präferenzen',
tool: 'Tools', skill: 'Skills', fact: 'Fakten',
conversation: 'Konversation', reminder: 'Reminder',
};
const TYPE_OPTIONS = ['', 'identity', 'rule', 'preference', 'tool', 'skill', 'fact', 'conversation', 'reminder'];
interface Props {
/** Wenn gesetzt: nur diese IDs anzeigen (z.B. Inbox-Modal mit Chat-Bubbles-Filter). */
restrictToIds?: string[];
/** Headline ueber der Liste. */
title?: string;
/** Style-Erweiterung fuer den Container. */
flatStyle?: boolean;
}
export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle }) => {
const [items, setItems] = useState<Memory[]>([]);
const [filtered, setFiltered] = useState<Memory[]>([]);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [q, setQ] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [pinnedFilter, setPinnedFilter] = useState<'all' | 'pinned' | 'cold'>('all');
const [showTypeMenu, setShowTypeMenu] = useState(false);
const [openId, setOpenId] = useState<string | null>(null);
const load = useCallback(() => {
setLoading(true); setErr(null);
brainApi.listMemories({ limit: 500 })
.then(setItems)
.catch(e => setErr(String(e?.message || e)))
.finally(() => setLoading(false));
}, []);
useEffect(() => { load(); }, [load]);
// Filter clientseitig — bei kleiner DB (<1000) easy
useEffect(() => {
let out = items;
if (restrictToIds && restrictToIds.length) {
const set = new Set(restrictToIds);
out = out.filter(m => set.has(m.id));
}
if (typeFilter) out = out.filter(m => m.type === typeFilter);
if (pinnedFilter === 'pinned') out = out.filter(m => m.pinned);
else if (pinnedFilter === 'cold') out = out.filter(m => !m.pinned);
if (q.trim()) {
const needle = q.toLowerCase();
out = out.filter(m =>
(m.title || '').toLowerCase().includes(needle) ||
(m.content || '').toLowerCase().includes(needle) ||
(m.category || '').toLowerCase().includes(needle) ||
(m.tags || []).some(t => t.toLowerCase().includes(needle))
);
}
setFiltered(out);
}, [items, q, typeFilter, pinnedFilter, restrictToIds]);
const onAddNew = () => {
Alert.prompt(
'Neue Memory',
'Titel:',
[
{ text: 'Abbrechen', style: 'cancel' },
{
text: 'Anlegen',
onPress: async (title?: string) => {
const t = (title || '').trim();
if (!t) return;
try {
const m = await brainApi.saveMemory({
type: 'fact', title: t,
content: '(noch leer — bitte editieren)',
});
load();
setOpenId(m.id);
} catch (e: any) {
Alert.alert('Fehler', String(e?.message || e));
}
},
},
],
'plain-text',
);
};
const renderItem = ({ item }: { item: Memory }) => {
const attCount = (item.attachments || []).length;
return (
<TouchableOpacity style={s.row} onPress={() => setOpenId(item.id)}>
<View style={{flex:1}}>
<Text style={s.rowTitle} numberOfLines={1}>
{item.pinned ? '📌 ' : ''}{item.title || '(ohne Titel)'}
{attCount > 0 ? <Text style={s.attBadge}>{` 📎${attCount}`}</Text> : null}
</Text>
<Text style={s.rowMeta} numberOfLines={1}>
{TYPE_LABELS[item.type] || item.type}
{item.category ? ` · [${item.category}]` : ''}
</Text>
<Text style={s.rowPreview} numberOfLines={2}>{item.content}</Text>
</View>
</TouchableOpacity>
);
};
return (
<View style={[s.container, flatStyle && {padding:0,backgroundColor:'transparent'}]}>
{title ? <Text style={s.heading}>{title}</Text> : null}
<View style={s.searchRow}>
<TextInput
style={s.search}
value={q}
onChangeText={setQ}
placeholder="Suche in Titel, Inhalt, Tags…"
placeholderTextColor="#555570"
/>
<TouchableOpacity style={s.iconBtn} onPress={load}>
<Text style={{color:'#0096FF'}}></Text>
</TouchableOpacity>
</View>
<View style={s.filterRow}>
<TouchableOpacity style={s.filterBtn} onPress={() => setShowTypeMenu(true)}>
<Text style={s.filterText}>{typeFilter ? (TYPE_LABELS[typeFilter] || typeFilter) : 'Alle Typen'} </Text>
</TouchableOpacity>
<TouchableOpacity style={s.filterBtn} onPress={() => {
setPinnedFilter(pinnedFilter === 'all' ? 'pinned' : pinnedFilter === 'pinned' ? 'cold' : 'all');
}}>
<Text style={s.filterText}>
{pinnedFilter === 'pinned' ? '📌 Nur Pinned' : pinnedFilter === 'cold' ? 'Nur Cold' : 'Alle'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={[s.filterBtn,{backgroundColor:'#0096FF'}]} onPress={onAddNew}>
<Text style={[s.filterText,{color:'#fff'}]}>+ Neu</Text>
</TouchableOpacity>
</View>
{err ? <Text style={s.err}>{err}</Text> : null}
{loading && items.length === 0 ? (
<ActivityIndicator color="#0096FF" style={{marginTop:20}} />
) : (
<FlatList
data={filtered}
keyExtractor={m => m.id}
renderItem={renderItem}
ListEmptyComponent={
<Text style={{color:'#555570',textAlign:'center',padding:20,fontStyle:'italic'}}>
{items.length === 0 ? '(keine Memories in der DB)' : '(keine Treffer für diese Filter)'}
</Text>
}
contentContainerStyle={{paddingBottom:20}}
/>
)}
<Text style={s.footer}>
{filtered.length}/{items.length} Memories
</Text>
{/* Type-Filter-Auswahl */}
<Modal visible={showTypeMenu} transparent animationType="fade" onRequestClose={() => setShowTypeMenu(false)}>
<TouchableOpacity style={s.menuBack} activeOpacity={1} onPress={() => setShowTypeMenu(false)}>
<View style={s.menuBox}>
{TYPE_OPTIONS.map(t => (
<TouchableOpacity
key={t || 'all'}
style={s.menuItem}
onPress={() => { setTypeFilter(t); setShowTypeMenu(false); }}
>
<Text style={s.menuItemText}>
{t ? (TYPE_LABELS[t] || t) : 'Alle Typen'}
</Text>
</TouchableOpacity>
))}
</View>
</TouchableOpacity>
</Modal>
<MemoryDetailModal
memoryId={openId}
visible={!!openId}
onClose={() => { setOpenId(null); load(); }}
onDeleted={() => { setOpenId(null); load(); }}
/>
</View>
);
};
const s = StyleSheet.create({
container: { flex:1, padding:8, backgroundColor:'#0D0D1A' },
heading: { color:'#0096FF', fontWeight:'bold', fontSize:14, marginBottom:8 },
searchRow: { flexDirection:'row', gap:6, marginBottom:6 },
search: { flex:1, backgroundColor:'#1E1E2E', color:'#E0E0F0', padding:8, borderRadius:6, fontSize:13 },
iconBtn: { paddingHorizontal:12, justifyContent:'center', backgroundColor:'#1E1E2E', borderRadius:6 },
filterRow: { flexDirection:'row', gap:6, marginBottom:8 },
filterBtn: { backgroundColor:'#1E1E2E', paddingHorizontal:10, paddingVertical:6, borderRadius:6 },
filterText: { color:'#E0E0F0', fontSize:12 },
err: { color:'#FF6B6B', fontSize:12, marginVertical:6 },
row: { backgroundColor:'#1E1E2E', padding:10, borderRadius:6, marginBottom:6 },
rowTitle: { color:'#E0E0F0', fontWeight:'600', fontSize:13 },
attBadge: { color:'#34C759', fontWeight:'normal', fontSize:11 },
rowMeta: { color:'#8888AA', fontSize:11, marginTop:2 },
rowPreview: { color:'#666680', fontSize:11, marginTop:4 },
footer: { color:'#555570', fontSize:10, textAlign:'center', paddingVertical:6 },
menuBack: { flex:1, backgroundColor:'rgba(0,0,0,0.7)', justifyContent:'center', alignItems:'center' },
menuBox: { backgroundColor:'#0D0D1A', borderRadius:8, paddingVertical:4, minWidth:200 },
menuItem: { paddingVertical:10, paddingHorizontal:14 },
menuItemText: { color:'#E0E0F0', fontSize:13 },
});
export default MemoryBrowser;
@@ -1,364 +0,0 @@
/**
* Memory-Detail-Modal — Anzeige + Edit eines einzelnen Memory-Eintrags.
*
* Zwei Modi:
* - read-only: zeigt alle Felder + Anhang-Vorschau (Klick auf Bild = Vollbild)
* - edit: Form mit Save/Delete/Anhang-hochladen
*
* Memory-Daten werden beim Oeffnen aus dem Brain (via brainApi → RVS) frisch
* gezogen. Optimistic Updates sind explizit nicht da — der DB-Stand ist die
* Truth.
*/
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
Image,
Modal,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import DocumentPicker, { DocumentPickerResponse } from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import brainApi, { Memory, MemoryAttachment } from '../services/brainApi';
interface Props {
memoryId: string | null;
visible: boolean;
onClose: () => void;
onDeleted?: (id: string) => void;
}
const TYPE_OPTIONS = [
{ value: 'identity', label: 'identity (FEST)' },
{ value: 'rule', label: 'rule (FEST)' },
{ value: 'preference', label: 'preference (FEST)' },
{ value: 'tool', label: 'tool (FEST)' },
{ value: 'skill', label: 'skill (FEST)' },
{ value: 'fact', label: 'fact (Cold)' },
{ value: 'conversation', label: 'conversation (Cold)' },
{ value: 'reminder', label: 'reminder (Cold)' },
];
export const MemoryDetailModal: React.FC<Props> = ({ memoryId, visible, onClose, onDeleted }) => {
const [memory, setMemory] = useState<Memory | null>(null);
const [loading, setLoading] = useState(false);
const [editing, setEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
// Edit-Felder
const [eTitle, setETitle] = useState('');
const [eContent, setEContent] = useState('');
const [eCategory, setECategory] = useState('');
const [eTags, setETags] = useState('');
const [ePinned, setEPinned] = useState(false);
// Bild-Vollbild
const [fullscreen, setFullscreen] = useState<string | null>(null);
// Memory laden beim Oeffnen
useEffect(() => {
if (!visible || !memoryId) {
setMemory(null); setEditing(false); setErr(null); return;
}
setLoading(true); setErr(null);
brainApi.getMemory(memoryId)
.then(m => {
setMemory(m);
setETitle(m.title || '');
setEContent(m.content || '');
setECategory(m.category || '');
setETags((m.tags || []).join(', '));
setEPinned(!!m.pinned);
})
.catch(e => setErr(String(e?.message || e)))
.finally(() => setLoading(false));
}, [visible, memoryId]);
const reload = () => {
if (!memoryId) return;
setLoading(true);
brainApi.getMemory(memoryId)
.then(m => setMemory(m))
.catch(e => setErr(String(e?.message || e)))
.finally(() => setLoading(false));
};
const onSave = async () => {
if (!memoryId) return;
setSaving(true); setErr(null);
try {
const tags = eTags.split(',').map(t => t.trim()).filter(Boolean);
const m = await brainApi.updateMemory(memoryId, {
title: eTitle.trim(),
content: eContent.trim(),
category: eCategory.trim(),
tags,
pinned: ePinned,
});
setMemory(m);
setEditing(false);
} catch (e: any) {
setErr(String(e?.message || e));
} finally {
setSaving(false);
}
};
const onDelete = () => {
if (!memoryId || !memory) return;
Alert.alert(
'Memory loeschen?',
`"${memory.title}"\n\nWird permanent aus der DB entfernt, inkl. aller Anhaenge.`,
[
{ text: 'Abbrechen', style: 'cancel' },
{
text: 'Loeschen',
style: 'destructive',
onPress: async () => {
try {
await brainApi.deleteMemory(memoryId);
if (onDeleted) onDeleted(memoryId);
onClose();
} catch (e: any) {
Alert.alert('Fehler', String(e?.message || e));
}
},
},
],
);
};
const onPickAndUpload = async () => {
if (!memoryId) return;
try {
const picked: DocumentPickerResponse[] = await DocumentPicker.pick({
type: [DocumentPicker.types.images, DocumentPicker.types.pdf, DocumentPicker.types.allFiles],
copyTo: 'cachesDirectory',
});
for (const f of picked) {
setBusy(`Lade ${f.name}`);
// RNFS lesen → base64 → API
const localPath = (f.fileCopyUri || f.uri).replace(/^file:\/\//, '');
const b64 = await RNFS.readFile(localPath, 'base64');
await brainApi.uploadAttachment(memoryId, f.name || 'datei', b64);
}
setBusy(null);
reload();
} catch (e: any) {
setBusy(null);
if (DocumentPicker.isCancel(e)) return;
Alert.alert('Upload-Fehler', String(e?.message || e));
}
};
const onDeleteAttachment = (att: MemoryAttachment) => {
if (!memoryId) return;
Alert.alert(
'Anhang loeschen?',
`"${att.name}"`,
[
{ text: 'Abbrechen', style: 'cancel' },
{
text: 'Loeschen',
style: 'destructive',
onPress: async () => {
try {
const m = await brainApi.deleteAttachment(memoryId, att.name);
setMemory(m);
} catch (e: any) {
Alert.alert('Fehler', String(e?.message || e));
}
},
},
],
);
};
const onTapAttachment = async (att: MemoryAttachment) => {
if (!memoryId) return;
if ((att.mime || '').startsWith('image/')) {
try {
setBusy('Lade Bild…');
const data = await brainApi.getAttachmentBytes(memoryId, att.name);
// Temp-File schreiben damit <Image source={uri: file://...}> es zeigen kann
const safe = att.name.replace(/[^A-Za-z0-9._-]/g, '_');
const localPath = `${RNFS.CachesDirectoryPath}/memory_${memoryId}_${safe}`;
await RNFS.writeFile(localPath, data.base64, 'base64');
setBusy(null);
setFullscreen('file://' + localPath);
} catch (e: any) {
setBusy(null);
Alert.alert('Fehler', String(e?.message || e));
}
} else {
Alert.alert('Anhang', `${att.name}\n${att.mime}\n${att.size} Byte\n\nPfad: ${att.path}`);
}
};
return (
<Modal visible={visible} animationType="slide" transparent onRequestClose={onClose}>
<View style={s.backdrop}>
<View style={s.box}>
<View style={s.header}>
<Text style={s.title}>{editing ? 'Memory bearbeiten' : 'Memory-Detail'}</Text>
<TouchableOpacity onPress={onClose} hitSlop={{top:8,bottom:8,left:8,right:8}}>
<Text style={s.closeX}>×</Text>
</TouchableOpacity>
</View>
<ScrollView style={s.body} contentContainerStyle={{paddingBottom:20}}>
{loading ? (
<ActivityIndicator color="#0096FF" style={{marginTop:30}} />
) : err && !memory ? (
<Text style={s.err}>{err}</Text>
) : memory ? (
editing ? (
<View>
<Text style={s.label}>Typ</Text>
<Text style={{color:'#888',fontSize:12,marginBottom:8}}>{memory.type} (kann hier nicht geaendert werden)</Text>
<Text style={s.label}>Titel</Text>
<TextInput style={s.input} value={eTitle} onChangeText={setETitle} />
<Text style={s.label}>Inhalt</Text>
<TextInput
style={[s.input, {minHeight:120, textAlignVertical:'top'}]}
value={eContent}
onChangeText={setEContent}
multiline
/>
<Text style={s.label}>Kategorie</Text>
<TextInput style={s.input} value={eCategory} onChangeText={setECategory} />
<Text style={s.label}>Tags (komma-getrennt)</Text>
<TextInput style={s.input} value={eTags} onChangeText={setETags} />
<View style={{flexDirection:'row',alignItems:'center',marginTop:10,gap:8}}>
<Switch value={ePinned} onValueChange={setEPinned} />
<Text style={{color:'#E0E0F0'}}>📌 Pinned (immer im System-Prompt)</Text>
</View>
{err ? <Text style={s.err}>{err}</Text> : null}
<View style={{flexDirection:'row',gap:8,marginTop:14}}>
<TouchableOpacity style={[s.btn,s.btnSecondary]} onPress={() => setEditing(false)} disabled={saving}>
<Text style={s.btnText}>Abbrechen</Text>
</TouchableOpacity>
<TouchableOpacity style={[s.btn,s.btnPrimary,{flex:1}]} onPress={onSave} disabled={saving}>
<Text style={s.btnText}>{saving ? 'Speichere…' : 'Speichern'}</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View>
<View style={{flexDirection:'row',alignItems:'flex-start',justifyContent:'space-between'}}>
<Text style={s.bigTitle}>{memory.pinned ? '📌 ' : ''}{memory.title}</Text>
<TouchableOpacity onPress={() => setEditing(true)} style={s.iconBtn}>
<Text style={s.iconBtnText}></Text>
</TouchableOpacity>
</View>
<Text style={s.meta}>
{memory.type}{memory.category ? ` · [${memory.category}]` : ''}
</Text>
{(memory.tags || []).length > 0 ? (
<View style={s.tagsRow}>
{memory.tags.map(t => <Text key={t} style={s.tag}>{t}</Text>)}
</View>
) : null}
<Text style={s.contentBlock}>{memory.content}</Text>
<Text style={s.sectionHead}>📎 Anhaenge</Text>
{(memory.attachments || []).length === 0 ? (
<Text style={{color:'#555570',fontStyle:'italic',fontSize:12}}>(keine)</Text>
) : (
(memory.attachments || []).map((a) => {
const isImage = (a.mime || '').startsWith('image/');
return (
<View key={a.name} style={s.attRow}>
<TouchableOpacity style={{flexDirection:'row',alignItems:'center',gap:8,flex:1}} onPress={() => onTapAttachment(a)}>
<Text style={{fontSize:18}}>{isImage ? '🖼️' : '📄'}</Text>
<View style={{flex:1}}>
<Text style={{color:'#E0E0F0',fontSize:12}} numberOfLines={1}>{a.name}</Text>
<Text style={{color:'#555570',fontSize:10}}>{a.mime} · {Math.round(a.size/1024)} KB</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => onDeleteAttachment(a)} style={s.attDelete}>
<Text style={{color:'#FF6B6B',fontSize:12}}>🗑</Text>
</TouchableOpacity>
</View>
);
})
)}
<TouchableOpacity style={[s.btn,s.btnSecondary,{marginTop:8}]} onPress={onPickAndUpload}>
<Text style={s.btnText}> Datei anhaengen</Text>
</TouchableOpacity>
{busy ? <Text style={{color:'#8888AA',fontSize:11,marginTop:4}}>{busy}</Text> : null}
<Text style={s.timestamps}>
angelegt: {(memory.created_at || '').slice(0,16).replace('T',' ')}{'\n'}
geaendert: {(memory.updated_at || '').slice(0,16).replace('T',' ')}{'\n'}
id: {memory.id}
</Text>
<TouchableOpacity style={[s.btn,s.btnDanger,{marginTop:14}]} onPress={onDelete}>
<Text style={s.btnText}>🗑 Memory komplett loeschen</Text>
</TouchableOpacity>
</View>
)
) : null}
</ScrollView>
</View>
</View>
<Modal visible={!!fullscreen} transparent onRequestClose={() => setFullscreen(null)}>
<TouchableOpacity style={s.fsBack} onPress={() => setFullscreen(null)}>
{fullscreen ? <Image source={{uri:fullscreen}} style={s.fsImg} resizeMode="contain" /> : null}
</TouchableOpacity>
</Modal>
</Modal>
);
};
const s = StyleSheet.create({
backdrop: { flex:1, backgroundColor:'rgba(0,0,0,0.75)', justifyContent:'flex-end' },
box: { backgroundColor:'#0D0D1A', borderTopLeftRadius:12, borderTopRightRadius:12, maxHeight:'92%' },
header: { flexDirection:'row', justifyContent:'space-between', alignItems:'center', padding:14, borderBottomColor:'#1E1E2E', borderBottomWidth:1 },
title: { color:'#FFD60A', fontWeight:'bold', fontSize:15 },
closeX: { color:'#8888AA', fontSize:24, paddingHorizontal:6 },
body: { padding:14 },
err: { color:'#FF6B6B', fontSize:12, marginTop:8 },
label: { color:'#8888AA', fontSize:11, marginBottom:3, marginTop:8 },
input: { backgroundColor:'#080810', borderColor:'#1E1E2E', borderWidth:1, borderRadius:4, padding:8, color:'#E0E0F0', fontSize:13 },
bigTitle: { color:'#E0E0F0', fontWeight:'bold', fontSize:16, flex:1, marginRight:6 },
iconBtn: { padding:6, backgroundColor:'#1E1E2E', borderRadius:6 },
iconBtnText: { color:'#0096FF', fontSize:14 },
meta: { color:'#8888AA', fontSize:11, marginTop:4 },
tagsRow: { flexDirection:'row', flexWrap:'wrap', gap:4, marginTop:6 },
tag: { backgroundColor:'#1E1E2E', color:'#8888AA', fontSize:10, paddingHorizontal:6, paddingVertical:2, borderRadius:8 },
contentBlock: { color:'#E0E0F0', fontSize:13, marginTop:12, lineHeight:18 },
sectionHead: { color:'#0096FF', fontSize:11, marginTop:14, marginBottom:6, textTransform:'uppercase', letterSpacing:0.5 },
attRow: { flexDirection:'row', alignItems:'center', backgroundColor:'#080810', padding:8, borderRadius:6, marginBottom:4, gap:6 },
attDelete: { padding:4 },
timestamps: { color:'#555570', fontSize:10, marginTop:12, fontFamily:'monospace' },
btn: { paddingVertical:10, paddingHorizontal:14, borderRadius:6, alignItems:'center' },
btnPrimary: { backgroundColor:'#0096FF' },
btnSecondary: { backgroundColor:'#1E1E2E' },
btnDanger: { backgroundColor:'#3B1010', borderWidth:1, borderColor:'#FF6B6B' },
btnText: { color:'#fff', fontSize:13, fontWeight:'600' },
fsBack: { flex:1, backgroundColor:'rgba(0,0,0,0.95)', justifyContent:'center', alignItems:'center' },
fsImg: { width:'95%', height:'85%' },
});
export default MemoryDetailModal;
+8 -300
View File
@@ -28,8 +28,6 @@ import RNFS from 'react-native-fs';
import { SvgUri } from 'react-native-svg';
import { Dimensions } from 'react-native';
import ZoomableImage from '../components/ZoomableImage';
import MemoryDetailModal from '../components/MemoryDetailModal';
import MemoryBrowser from '../components/MemoryBrowser';
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
import audioService from '../services/audio';
import wakeWordService from '../services/wakeword';
@@ -89,25 +87,6 @@ interface ChatMessage {
fires_at?: string;
condition?: string;
};
/** Memory-Saved-Bubble: ARIA hat etwas via memory_save in die Qdrant-DB gepackt */
memorySaved?: {
id?: string;
title: string;
type: string;
category?: string;
pinned: boolean;
preview?: string;
/** Was passiert ist: angelegt / geaendert / geloescht. Default created
* fuer Rueckwaerts-Kompatibilitaet mit aelteren Events. */
action?: 'created' | 'updated' | 'deleted';
attachments?: Array<{
name: string;
mime?: string;
size?: number;
path?: string; // Server-Pfad /shared/memory-attachments/<id>/<name>
localUri?: string; // Nach file_request gefuelltes file://-URI
}>;
};
/** Backup-Timestamp aus chat_backup.jsonl auf dem Bridge — Voraussetzung
* zum Loeschen der Bubble via Muelltonne. Lokale Bubbles ohne backupTs
* sind noch nicht persistiert (kurzer Race) — Muelltonne erscheint erst
@@ -233,8 +212,6 @@ const ChatScreen: React.FC = () => {
// Genauer State (off/armed/conversing) fuer UI-Feedback am Button
const [wakeWordState, setWakeWordState] = useState<'off' | 'armed' | 'conversing'>('off');
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
const [inboxVisible, setInboxVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchVisible, setSearchVisible] = useState(false);
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
@@ -490,7 +467,6 @@ const ChatScreen: React.FC = () => {
const localOnly = prev.filter(m =>
m.skillCreated ||
m.triggerCreated ||
m.memorySaved ||
(m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...'))
);
// Server-Stand + lokal-only (chronologisch sortiert)
@@ -545,36 +521,6 @@ const ChatScreen: React.FC = () => {
return;
}
// memory_saved: ARIA hat etwas via memory_save Tool in die Qdrant-DB
// gepackt — eigene Bubble (gelb wie trigger/skill).
if (message.type === 'memory_saved') {
const p = (message.payload || {}) as any;
const atts = Array.isArray(p.attachments) ? p.attachments.map((a: any) => ({
name: String(a?.name || 'datei'),
mime: a?.mime ? String(a.mime) : undefined,
size: typeof a?.size === 'number' ? a.size : undefined,
path: a?.path ? String(a.path) : undefined,
})) : [];
const memoryMsg: ChatMessage = {
id: nextId(),
sender: 'aria',
text: '',
timestamp: Date.now(),
memorySaved: {
id: p.id ? String(p.id) : undefined,
title: String(p.title || '(ohne Titel)'),
type: String(p.type || 'fact'),
category: p.category ? String(p.category) : undefined,
pinned: !!p.pinned,
preview: p.content_preview ? String(p.content_preview) : undefined,
action: (p.action === 'updated' || p.action === 'deleted') ? p.action : 'created',
attachments: atts.length ? atts : undefined,
},
};
setMessages(prev => capMessages([...prev, memoryMsg]));
return;
}
// file_deleted: Datei wurde geloescht (vom Diagnostic User) → Bubble updaten
if (message.type === 'file_deleted') {
const p = (message.payload?.path as string) || '';
@@ -619,38 +565,16 @@ const ChatScreen: React.FC = () => {
if (b64 && reqId) {
const fileName = (message.payload.name as string) || 'download';
persistAttachment(b64, reqId, fileName).then(filePath => {
setMessages(prev => prev.map(m => {
// Hauptattachments updaten (Bilder/Files am User-Send / ARIA-File-Bubble)
const updatedAtts = m.attachments?.map(a =>
setMessages(prev => prev.map(m => ({
...m,
attachments: m.attachments?.map(a =>
a.serverPath === serverPath ? { ...a, uri: filePath } : a
);
// Memory-Anhang-Match (Bubble vom memory_saved-Event)
const ms = m.memorySaved;
let updatedMs = ms;
if (ms && Array.isArray(ms.attachments)) {
const hit = ms.attachments.some(a => a.path === serverPath);
if (hit) {
updatedMs = {
...ms,
attachments: ms.attachments.map(a =>
a.path === serverPath ? { ...a, localUri: filePath } : a
),
};
}
}
return { ...m, attachments: updatedAtts, memorySaved: updatedMs };
}));
),
})));
// Wenn der User dieses File explizit oeffnen wollte → Intent-Picker
// (Bilder werden separat via setFullscreenImage in der memorySaved-
// Bubble geoeffnet, das laeuft nicht ueber autoOpenPaths)
if (serverPath && autoOpenPaths.current.has(serverPath)) {
autoOpenPaths.current.delete(serverPath);
const isImage = (mimeType || '').startsWith('image/');
if (isImage) {
setFullscreenImage(filePath);
} else {
openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType);
}
openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType);
}
}).catch(() => {});
}
@@ -1026,15 +950,7 @@ const ChatScreen: React.FC = () => {
}, [messages]);
// Inverted FlatList: neueste Nachrichten unten, kein manuelles Scrollen noetig
// Spezial-Bubbles (memorySaved/triggerCreated/skillCreated) sollen im Chat
// NICHT mehr erscheinen — sie werden in der Notizen-Inbox angezeigt.
// Das verhindert dass sie chronologisch unten im Chat haengen und der
// eigentliche Chat-Verlauf darunter verschwindet.
const chatVisibleMessages = useMemo(
() => messages.filter(m => !m.memorySaved && !m.triggerCreated && !m.skillCreated),
[messages],
);
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
const invertedMessages = useMemo(() => [...messages].reverse(), [messages]);
// Such-Treffer: alle Message-IDs die zur Query passen, in chronologischer
// Reihenfolge (aelteste zuerst). Bei Query-Change resetten wir den Index.
@@ -1337,71 +1253,6 @@ const ChatScreen: React.FC = () => {
? { borderWidth: 2, borderColor: '#FFD60A' }
: null;
// Spezial-Bubble: ARIA hat etwas via memory_save gespeichert
if (item.memorySaved) {
const m = item.memorySaved;
const catPart = m.category ? ` · [${m.category}]` : '';
const atts = m.attachments || [];
const action = m.action || 'created';
const headline =
action === 'updated' ? '🧠 ARIA hat eine Notiz geändert' :
action === 'deleted' ? '🧠 ARIA hat eine Notiz gelöscht' :
'🧠 ARIA hat etwas gemerkt';
const headlineColor = action === 'deleted' ? '#FF6B6B' : '#FFD60A';
const borderColor = action === 'deleted' ? '#FF6B6B' : '#FFD60A';
const openable = !!m.id && action !== 'deleted';
const Wrapper: any = openable ? TouchableOpacity : View;
const wrapperProps = openable
? { onPress: () => setMemoryDetailId(m.id || null), activeOpacity: 0.7 }
: {};
return (
<Wrapper {...wrapperProps} style={[styles.messageBubble, styles.ariaBubble, {borderLeftWidth: 3, borderLeftColor: borderColor}, searchHighlightStyle]}>
<Text style={{color: headlineColor, fontWeight: 'bold', fontSize: 14}}>
{headline}
</Text>
<Text style={{color: '#E0E0F0', marginTop: 4, fontSize: 14}}>
<Text style={{fontWeight: 'bold'}}>{m.title}</Text>
<Text style={{color: '#8888AA', fontSize: 12}}>{` (${m.type}${m.pinned ? ' · 📌 pinned' : ''}${catPart})`}</Text>
</Text>
{m.preview ? (
<Text style={{color: '#888', fontSize: 12, marginTop: 4}}>{m.preview}{m.preview.length >= 140 ? '…' : ''}</Text>
) : null}
{atts.map((a, idx) => {
const isImage = (a.mime || '').startsWith('image/');
const icon = isImage ? '🖼️' : '📄';
const sizeStr = a.size ? ` · ${(a.size / 1024).toFixed(0)} KB` : '';
return (
<TouchableOpacity
key={`${item.id}-att-${idx}`}
style={styles.memoryAttachmentRow}
onPress={() => {
if (!a.path) return;
if (a.localUri) {
if (isImage) setFullscreenImage(a.localUri);
else openFileWithIntent(a.localUri.replace(/^file:\/\//, ''), a.mime || '');
} else {
// 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.memoryAttachmentName} numberOfLines={1}>{a.name}</Text>
<Text style={styles.memoryAttachmentMeta}>
{a.localUri ? '(tippen zum oeffnen)' : `(tippen zum Laden${sizeStr})`}
</Text>
</TouchableOpacity>
);
})}
<Text style={{color: '#555570', fontSize: 10, marginTop: 6}}>
ARIA-Memory · {time}{openable ? ' · tippen für Details' : ''}
</Text>
</Wrapper>
);
}
// Spezial-Bubble: ARIA hat einen Trigger angelegt
if (item.triggerCreated) {
const t = item.triggerCreated;
@@ -1604,10 +1455,7 @@ const ChatScreen: React.FC = () => {
{connectionState === 'connected' ? 'Verbunden' :
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'}
</Text>
<TouchableOpacity onPress={() => setInboxVisible(true)} style={{marginLeft: 'auto', paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
<Text style={{fontSize: 18}}>{'\uD83D\uDDC2\uFE0F'}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSearchVisible(!searchVisible)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
<TouchableOpacity onPress={() => setSearchVisible(!searchVisible)} style={{marginLeft: 'auto', paddingHorizontal: 8}}>
<Text style={{fontSize: 16}}>{'\uD83D\uDD0D'}</Text>
</TouchableOpacity>
</View>
@@ -1838,105 +1686,6 @@ const ChatScreen: React.FC = () => {
)}
</View>
{/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */}
<MemoryDetailModal
memoryId={memoryDetailId}
visible={!!memoryDetailId}
onClose={() => setMemoryDetailId(null)}
onDeleted={() => setMemoryDetailId(null)}
/>
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-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. */}
<Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}>
<View style={{flex:1, backgroundColor:'#0D0D1A'}}>
<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>
<TouchableOpacity onPress={() => setInboxVisible(false)} hitSlop={{top:8,bottom:8,left:8,right:8}}>
<Text style={{color:'#8888AA', fontSize:24}}>×</Text>
</TouchableOpacity>
</View>
{/* Aus aktuellem Chat: Spezial-Bubbles (memory/trigger/skill) kompakt
auflisten — neueste oben. Klick auf Memory oeffnet Detail-Modal. */}
{(() => {
const specials = messages
.filter(m => m.memorySaved || m.triggerCreated || m.skillCreated)
.slice().reverse();
if (specials.length === 0) {
return (
<View style={{padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
<Text style={{color:'#555570', fontSize:11, fontStyle:'italic'}}>
(keine Notizen-Bubbles im aktuellen Chat)
</Text>
</View>
);
}
return (
<View style={{maxHeight:260, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:8, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
Aus diesem Chat
</Text>
<ScrollView style={{paddingHorizontal:8}}>
{specials.map(m => {
if (m.memorySaved) {
const ms = m.memorySaved;
const action = ms.action || 'created';
const verb = action === 'updated' ? 'geändert' : action === 'deleted' ? 'gelöscht' : 'angelegt';
const dotColor = action === 'deleted' ? '#FF6B6B' : '#FFD60A';
return (
<TouchableOpacity
key={m.id}
style={styles.inboxRow}
onPress={() => { if (ms.id && action !== 'deleted') { setInboxVisible(false); setMemoryDetailId(ms.id); } }}
disabled={!ms.id || action === 'deleted'}
>
<Text style={{fontSize:16}}>{'🧠'}</Text>
<View style={{flex:1}}>
<Text style={styles.inboxRowTitle} numberOfLines={1}>{ms.title}</Text>
<Text style={[styles.inboxRowMeta, {color: dotColor}]}>Memory · {verb} · {ms.type}</Text>
</View>
{ms.id && action !== 'deleted' ? <Text style={{color:'#0096FF', fontSize:14}}></Text> : null}
</TouchableOpacity>
);
}
if (m.triggerCreated) {
const t = m.triggerCreated;
return (
<View key={m.id} style={styles.inboxRow}>
<Text style={{fontSize:16}}>{'⏰'}</Text>
<View style={{flex:1}}>
<Text style={styles.inboxRowTitle} numberOfLines={1}>{t.name}</Text>
<Text style={styles.inboxRowMeta}>Trigger · {t.type}{t.fires_at ? ` · ${t.fires_at.slice(0,16).replace('T',' ')}` : ''}</Text>
</View>
</View>
);
}
if (m.skillCreated) {
const sk = m.skillCreated;
return (
<View key={m.id} style={styles.inboxRow}>
<Text style={{fontSize:16}}>{'🛠'}</Text>
<View style={{flex:1}}>
<Text style={styles.inboxRowTitle} numberOfLines={1}>{sk.name}</Text>
<Text style={styles.inboxRowMeta}>Skill · {sk.execution}</Text>
</View>
</View>
);
}
return null;
})}
</ScrollView>
</View>
);
})()}
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:10, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
Alle Memories aus der DB
</Text>
<MemoryBrowser />
</View>
</Modal>
{/* Bild-Vollbild Modal */}
<Modal visible={!!fullscreenImage} transparent animationType="fade" onRequestClose={() => setFullscreenImage(null)}>
<View style={styles.fullscreenOverlay}>
@@ -2265,47 +2014,6 @@ const styles = StyleSheet.create({
playButtonText: {
fontSize: 16,
},
inboxRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
backgroundColor: '#1E1E2E',
padding: 10,
borderRadius: 6,
marginBottom: 4,
},
inboxRowTitle: {
color: '#E0E0F0',
fontSize: 13,
fontWeight: '600',
},
inboxRowMeta: {
color: '#8888AA',
fontSize: 11,
marginTop: 1,
},
memoryAttachmentRow: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#0D0D1A',
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 6,
marginTop: 4,
gap: 6,
},
memoryAttachmentIcon: {
fontSize: 16,
},
memoryAttachmentName: {
flex: 1,
color: '#E0E0F0',
fontSize: 12,
},
memoryAttachmentMeta: {
color: '#555570',
fontSize: 10,
},
bubbleTrash: {
position: 'absolute',
top: 4,
-14
View File
@@ -52,7 +52,6 @@ import {
} from '../services/audio';
import audioService from '../services/audio';
import gpsTrackingService from '../services/gpsTracking';
import MemoryBrowser from '../components/MemoryBrowser';
import { isVerboseLogging, setVerboseLogging } from '../services/logger';
import {
isWakeReadySoundEnabled,
@@ -101,7 +100,6 @@ const SETTINGS_SECTIONS = [
{ id: 'voice_output', icon: '🔊', label: 'Sprachausgabe', desc: 'Stimmen, Pre-Roll, Geschwindigkeit' },
{ id: 'storage', icon: '📁', label: 'Speicher', desc: 'Anhang-Speicherort, Auto-Download' },
{ id: 'files', icon: '📂', label: 'Dateien', desc: 'ARIA- und User-Dateien — anzeigen, löschen' },
{ id: 'memory', icon: '🧠', label: 'Gedächtnis', desc: 'ARIA-Memories durchsuchen, anlegen, bearbeiten, löschen' },
{ id: 'protocol', icon: '📜', label: 'Protokoll', desc: 'Privatsphaere, Backup' },
{ id: 'about', icon: '️', label: 'Ueber', desc: 'App-Version, Update' },
] as const;
@@ -1675,18 +1673,6 @@ const SettingsScreen: React.FC = () => {
</View>
</>)}
{/* === Gedaechtnis === */}
{currentSection === 'memory' && (<>
<Text style={styles.sectionTitle}>Gedächtnis</Text>
<Text style={{color: '#8888AA', fontSize: 12, marginBottom: 8, paddingHorizontal: 4}}>
Alle Memory-Einträge aus ARIAs Vector-DB. Tippen zum Bearbeiten mit Anhängen, pinned-Status,
Tags. Neue Einträge anlegen via "+ Neu".
</Text>
<View style={{height: 600, marginBottom: 8}}>
<MemoryBrowser />
</View>
</>)}
{/* === Logs === */}
{currentSection === 'protocol' && (<>
<Text style={styles.sectionTitle}>Protokoll</Text>
-206
View File
@@ -1,206 +0,0 @@
/**
* Brain-API-Client fuer die App.
*
* Die App hat keinen direkten HTTP-Zugriff aufs Brain (nur via RVS). Wir
* tunneln alle Memory-Operationen ueber den generischen brain_request /
* brain_response RVS-Channel den die Bridge implementiert.
*
* Pattern: pro Call eine eindeutige requestId, Listener wartet auf passende
* brain_response, Promise loest auf / wird abgelehnt bei status>=400.
*/
import rvs from './rvs';
type AnyJson = any;
interface PendingRequest {
resolve: (data: AnyJson) => void;
reject: (err: Error) => void;
timer: ReturnType<typeof setTimeout>;
expectBinary?: boolean;
}
const pending = new Map<string, PendingRequest>();
let installed = false;
function _ensureListener() {
if (installed) return;
installed = true;
rvs.onMessage((msg: any) => {
if (!msg || msg.type !== 'brain_response') return;
const p = msg.payload || {};
const reqId: string = p.requestId || '';
const handler = pending.get(reqId);
if (!handler) return;
pending.delete(reqId);
clearTimeout(handler.timer);
const status: number = Number(p.status || 0);
if (status >= 200 && status < 300) {
if (handler.expectBinary) {
handler.resolve({ base64: p.base64 || '', contentType: p.contentType || '' });
} else {
handler.resolve(p.json !== undefined ? p.json : (p.text !== undefined ? p.text : null));
}
} else {
const detail = (p.json && p.json.detail) || p.text || `HTTP ${status}`;
handler.reject(new Error(`Brain ${status}: ${detail}`));
}
});
}
let _nextId = 0;
function _newRequestId(): string {
_nextId += 1;
return `brain_${Date.now().toString(36)}_${_nextId}`;
}
interface SendOpts {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
body?: AnyJson;
bodyBase64?: string;
contentType?: string;
expectBinary?: boolean;
timeoutMs?: number;
}
function _send(path: string, opts: SendOpts = {}): Promise<AnyJson> {
_ensureListener();
return new Promise((resolve, reject) => {
const requestId = _newRequestId();
const timer = setTimeout(() => {
if (pending.delete(requestId)) {
reject(new Error(`Brain-Timeout fuer ${path}`));
}
}, opts.timeoutMs || 30000);
pending.set(requestId, { resolve, reject, timer, expectBinary: opts.expectBinary });
rvs.send('brain_request' as any, {
requestId,
method: opts.method || 'GET',
path,
...(opts.body !== undefined ? { body: opts.body } : {}),
...(opts.bodyBase64 ? { bodyBase64: opts.bodyBase64 } : {}),
...(opts.contentType ? { contentType: opts.contentType } : {}),
});
});
}
// ── Typen ────────────────────────────────────────────────────────────
export interface MemoryAttachment {
name: string;
mime: string;
size: number;
path: string;
}
export interface Memory {
id: string;
type: string;
title: string;
content: string;
pinned: boolean;
category: string;
source: string;
tags: string[];
created_at: string;
updated_at: string;
conversation_id?: string | null;
score?: number | null;
attachments?: MemoryAttachment[];
}
// ── Memory CRUD ──────────────────────────────────────────────────────
export const brainApi = {
/** Einzelne Memory holen (mit allen Feldern inkl. Anhaenge) */
getMemory(id: string): Promise<Memory> {
return _send(`/memory/get/${encodeURIComponent(id)}`);
},
/** Liste aller Memories, optional nach Type gefiltert. */
listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> {
const qs = new URLSearchParams();
if (opts.type) qs.set('type', opts.type);
qs.set('limit', String(opts.limit || 500));
return _send(`/memory/list?${qs.toString()}`);
},
/** Volltext-Substring-Suche. */
searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> {
const qs = new URLSearchParams({ q });
if (opts.type) qs.set('type', opts.type);
qs.set('include_pinned', String(opts.includePinned !== false));
qs.set('k', String(opts.k || 50));
return _send(`/memory/search-text?${qs.toString()}`);
},
/** Semantische Suche (Embedder). */
searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> {
const qs = new URLSearchParams({ q });
if (opts.type) qs.set('type', opts.type);
qs.set('include_pinned', String(opts.includePinned !== false));
qs.set('k', String(opts.k || 10));
qs.set('score_threshold', String(opts.threshold ?? 0.30));
return _send(`/memory/search?${qs.toString()}`);
},
/** Memory anlegen. */
saveMemory(body: {
type: string;
title: string;
content: string;
pinned?: boolean;
category?: string;
tags?: string[];
}): Promise<Memory> {
return _send('/memory/save', {
method: 'POST',
body: { source: 'app', ...body },
});
},
/** Memory aktualisieren (Patch — nur uebergebene Felder werden geaendert). */
updateMemory(id: string, body: Partial<Pick<Memory, 'title' | 'content' | 'pinned' | 'category' | 'tags'>>): Promise<Memory> {
return _send(`/memory/update/${encodeURIComponent(id)}`, {
method: 'PATCH',
body,
});
},
/** Memory loeschen. */
deleteMemory(id: string): Promise<{ deleted: string }> {
return _send(`/memory/delete/${encodeURIComponent(id)}`, {
method: 'DELETE',
timeoutMs: 15000,
});
},
// ── Anhaenge ────────────────────────────────────────────────────────
/** Datei als Anhang an die Memory haengen (Base64-Upload). */
uploadAttachment(memoryId: string, name: string, base64: string): Promise<Memory> {
return _send(`/memory/${encodeURIComponent(memoryId)}/attachments`, {
method: 'POST',
body: { name, data_base64: base64 },
timeoutMs: 120000,
});
},
/** Anhang loeschen. */
deleteAttachment(memoryId: string, filename: string): Promise<Memory> {
return _send(
`/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`,
{ method: 'DELETE' },
);
},
/** Anhang-Bytes holen (fuer Vorschau / Download). Liefert Base64. */
getAttachmentBytes(memoryId: string, filename: string): Promise<{ base64: string; contentType: string }> {
return _send(
`/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`,
{ expectBinary: true, timeoutMs: 60000 },
);
},
};
export default brainApi;
+2 -288
View File
@@ -206,126 +206,6 @@ META_TOOLS = [
},
},
},
{
"type": "function",
"function": {
"name": "memory_search",
"description": (
"Durchsuche aktiv dein Gedaechtnis (Qdrant-DB). Nutze das wenn:\n"
"- der User sagt 'schau in deinem Gedaechtnis' / 'ich hab das Memory aktualisiert'\n"
"- du dir bei einer Info aus dem Konversations-Verlauf unsicher bist "
"(z.B. ob das noch der aktuelle Stand ist)\n"
"- du pruefen willst ob's schon einen Memory zu einem Thema gibt bevor "
"du via memory_save einen neuen anlegst (vermeidet Fragmentierung)\n\n"
"**WICHTIG: Memory ist Truth ueber dem Conversation-Window.** "
"Wenn dort was anders steht als in deinem Gespraechs-Verlauf, gilt das "
"was im Memory steht — der User koennte gerade was korrigiert haben.\n\n"
"Mode 'text' = Substring (case-insensitive), gut fuer exakte Begriffe "
"wie 'cessna'. Mode 'semantic' = Embedder-Search, gut fuer 'wann hatten "
"wir ueber X gesprochen'-Fragen."
),
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Such-Begriff"},
"mode": {
"type": "string",
"enum": ["text", "semantic"],
"description": "Default 'text' (Substring). 'semantic' fuer aehnlichkeits-Suche.",
},
"k": {"type": "integer", "description": "Wieviele Treffer (Default 5, max 20)"},
},
"required": ["query"],
},
},
},
{
"type": "function",
"function": {
"name": "memory_update",
"description": (
"Aktualisiere einen existierenden Memory-Eintrag — gibt die ID aus "
"memory_search oder dem Cold-Memory an. Nur die uebergebenen Felder werden "
"ueberschrieben, der Rest bleibt unangetastet. **Bevorzuge das ueber "
"memory_save** wenn der User eine Korrektur macht oder du zusaetzliche "
"Details zum gleichen Thema hast — vermeidet doppelte Eintraege."
),
"parameters": {
"type": "object",
"properties": {
"id": {"type": "string", "description": "Memory-ID (UUID, aus memory_search oder Cold-Memory)"},
"title": {"type": "string", "description": "Neuer Titel (optional)"},
"content": {"type": "string", "description": "Neuer Content — wird neu embedded fuer Search (optional)"},
"category": {"type": "string", "description": "Neue Kategorie (optional)"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "Neue Tags (ueberschreibt komplett)"},
"pinned": {"type": "boolean", "description": "Pinning aendern (optional)"},
},
"required": ["id"],
},
},
},
{
"type": "function",
"function": {
"name": "memory_save",
"description": (
"Speichere eine Information dauerhaft in deinem Gedaechtnis (Qdrant-DB). "
"Nutze das wenn Stefan 'merk dir das' sagt oder du selbst etwas Wichtiges "
"festhalten willst. ALTERNATIVEN VERMEIDEN: du hast KEIN persistentes "
"File-Memory mehr — schreibe nicht in `~/.claude/projects/...`, das ist tot.\n\n"
"Type-Wahl:\n"
"- identity: ARIAs Selbstbild / Wesensart (PINNED)\n"
"- rule: harte Regel / Sicherheit / Werte (PINNED)\n"
"- preference: Stefans Vorlieben/Arbeitsweise (PINNED)\n"
"- tool: Tool-Freigaben / Infrastruktur (PINNED)\n"
"- skill: Faehigkeit / Workflow-Anleitung (PINNED)\n"
"- fact: Wissen ueber Stefan/Welt/Sachen (Vorlieben, Besitz, Orte, "
"Termine, Personen). Cold Memory, kommt nur via Semantic Search "
"rein. **Default fuer 'merk-dir-das'-Anfragen.**\n"
"- reminder: Termin/Aufgabe. Fuer ARIA-soll-ausloesen lieber trigger_timer.\n\n"
"Wenn unsicher: type=fact, pinned=false.\n\n"
"### Anhaenge\n"
"`attach_paths` haengt Dateien (Bilder, PDFs, ...) aus `/shared/uploads/` "
"an die Memory. Pfade kommen typischerweise aus dem Chat (Stefan haengt "
"ein Foto an, du siehst den Pfad in der User-Message).\n\n"
"**WICHTIG vor dem Speichern bei Bildern**: Schau dir das Bild ZUERST "
"an mit `Read <pfad>` (dein Read-Tool ist multi-modal — es liest Bilder "
"wie Vision-API). Extrahiere alles Relevante in den content: sichtbare "
"Texte, Marken/Modelle, Kennzeichen/Seriennummern, Personen, Orte, "
"auffaellige Details. Dann erst memory_save mit dem extrahierten "
"content + attach_paths fuer das Bild. So weisst du beim spaeteren "
"Cold-Memory-Lookup was im Bild war, ohne es nochmal lesen zu muessen.\n\n"
"Beispiel-Workflow:\n"
"1. User: 'Ich hab eine Cessna 172' + /shared/uploads/aria_xy.jpg\n"
"2. Du: `Read /shared/uploads/aria_xy.jpg` → siehst Foto, erkennst Kennung D-EAAA\n"
"3. Du: `memory_save(type='fact', title='Stefans Cessna 172', "
"content='Stefan besitzt eine Cessna 172, Kennung D-EAAA, "
"weiss/rot lackiert, vor Hangar fotografiert.', "
"attach_paths=['/shared/uploads/aria_xy.jpg'])`"
),
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "Kurzer Titel (max ~80 Zeichen)"},
"content": {"type": "string", "description": "Der eigentliche Inhalt — wird embedded fuer Semantic Search. Bei Bildern: extrahierte Infos REINSCHREIBEN (Texte, Kennungen, Marken, etc.)"},
"type": {
"type": "string",
"enum": ["identity", "rule", "preference", "tool", "skill", "fact", "conversation", "reminder"],
"description": "Memory-Typ (siehe oben)",
},
"category": {"type": "string", "description": "Optional, freier Tag z.B. 'meine-sachen', 'kunden', 'persoenlichkeit'"},
"tags": {"type": "array", "items": {"type": "string"}, "description": "Optionale Tags"},
"pinned": {"type": "boolean", "description": "Default false. Nur true wenn die Info IMMER im System-Prompt liegen muss (Identitaet/Regeln/Praeferenzen)."},
"attach_paths": {
"type": "array",
"items": {"type": "string"},
"description": "Optional. Pfade unter /shared/uploads/ die als Anhang an die Memory wandern. Files werden serverseitig nach /shared/memory-attachments/<id>/ kopiert — Originale bleiben.",
},
},
"required": ["title", "content", "type"],
},
},
},
]
@@ -361,14 +241,6 @@ def _skill_to_tool(s: dict) -> dict:
class Agent:
# Mindest-Score den ein Cold-Memory-Treffer haben muss um in den
# System-Prompt aufgenommen zu werden. Unter dieser Schwelle ist's
# Rauschen — die MiniLM-multilingual Embeddings haben fuer "irgendwas
# vs. irgendwas anderes" gerne mal 0.10-0.20 Score selbst bei voellig
# unverwandten Inhalten. Mit 0.30 als Untergrenze vermeiden wir
# Cross-Talk (z.B. 'hab ich ein flugzeug' triggert die Firmenadresse).
COLD_SCORE_THRESHOLD = 0.30
def __init__(self, store: VectorStore, embedder: Embedder,
conversation: Conversation, proxy: ProxyClient,
cold_k: int = 5):
@@ -406,13 +278,10 @@ class Agent:
# 2. Hot Memory (alle pinned Punkte)
hot = self.store.list_pinned()
# 3. Cold Memory (Top-K semantic) — mit Score-Threshold gegen Rauschen
# 3. Cold Memory (Top-K semantic)
try:
qvec = self.embedder.embed(user_message)
cold = self.store.search(
qvec, k=self.cold_k, exclude_pinned=True,
score_threshold=self.COLD_SCORE_THRESHOLD,
)
cold = self.store.search(qvec, k=self.cold_k, exclude_pinned=True)
except Exception as exc:
logger.warning("Cold-Search fehlgeschlagen: %s", exc)
cold = []
@@ -598,161 +467,6 @@ class Agent:
else:
lines.append(f"- {t['name']} ({t['type']}, {state})")
return "\n".join(lines)
if name == "memory_search":
query = (arguments.get("query") or "").strip()
if not query:
return "FEHLER: query ist Pflicht."
mode = arguments.get("mode") or "text"
try:
k = int(arguments.get("k", 5))
except (TypeError, ValueError):
k = 5
k = max(1, min(k, 20))
try:
if mode == "semantic":
qvec = self.embedder.embed(query)
results = self.store.search(
qvec, k=k, exclude_pinned=False, score_threshold=0.30,
)
else:
results = self.store.search_text(query, k=k, exclude_pinned=False)
if not results:
return f"Keine Treffer fuer '{query}' (mode={mode})."
lines = [f"{len(results)} Treffer fuer '{query}' (mode={mode}):"]
for m in results:
score_part = f" [score={m.score:.2f}]" if m.score is not None else ""
pin = "📌 " if m.pinned else ""
atts = m.attachments or []
att_part = f" 📎{len(atts)}" if atts else ""
lines.append("")
lines.append(f"## {pin}{m.title} ({m.type}){score_part}{att_part}")
lines.append(f"id: {m.id}")
lines.append(m.content or "")
if atts:
for a in atts:
lines.append(f" 📎 {a.get('name', '?')} ({a.get('mime', '')}) — {a.get('path', '')}")
return "\n".join(lines)
except Exception as e:
logger.exception("memory_search fehlgeschlagen")
return f"FEHLER: {e}"
if name == "memory_update":
pid = (arguments.get("id") or "").strip()
if not pid:
return "FEHLER: id ist Pflicht."
existing = self.store.get(pid)
if not existing:
return f"FEHLER: Memory mit id={pid[:8]} nicht gefunden."
try:
from memory.vector_store import COLLECTION
import datetime as _dt
content_changed = False
if "title" in arguments and arguments["title"] is not None:
existing.title = str(arguments["title"]).strip()
if "content" in arguments and arguments["content"] is not None:
new_content = str(arguments["content"]).strip()
if new_content != existing.content:
content_changed = True
existing.content = new_content
if "category" in arguments and arguments["category"] is not None:
existing.category = str(arguments["category"]).strip()
if "tags" in arguments and arguments["tags"] is not None:
existing.tags = [str(t).strip() for t in (arguments["tags"] or []) if str(t).strip()]
if "pinned" in arguments and arguments["pinned"] is not None:
existing.pinned = bool(arguments["pinned"])
existing.updated_at = _dt.datetime.now(_dt.timezone.utc).isoformat()
if content_changed:
vec = self.embedder.embed(existing.content)
self.store.upsert(existing, vec)
else:
self.store.client.set_payload(
collection_name=COLLECTION,
payload=existing.to_payload() | {"updated_at": existing.updated_at},
points=[pid],
)
saved = self.store.get(pid)
self._pending_events.append({
"type": "memory_saved",
"action": "updated",
"memory": {
"id": saved.id, "type": saved.type, "title": saved.title,
"content_preview": (saved.content or "")[:140],
"category": saved.category, "pinned": saved.pinned,
"attachments": saved.attachments or [],
},
})
return f"OK — Memory '{saved.title}' aktualisiert (id={pid[:8]})."
except Exception as e:
logger.exception("memory_update fehlgeschlagen")
return f"FEHLER: {e}"
if name == "memory_save":
title = (arguments.get("title") or "").strip()
content = (arguments.get("content") or "").strip()
mem_type = (arguments.get("type") or "fact").strip()
if not title or not content:
return "FEHLER: title und content sind Pflicht."
valid_types = {"identity", "rule", "preference", "tool",
"skill", "fact", "conversation", "reminder"}
if mem_type not in valid_types:
return f"FEHLER: type muss einer von {sorted(valid_types)} sein."
category = (arguments.get("category") or "").strip()
tags_in = arguments.get("tags") or []
tags = [str(t).strip() for t in tags_in if str(t).strip()] if isinstance(tags_in, list) else []
pinned = bool(arguments.get("pinned", False))
attach_paths_in = arguments.get("attach_paths") or []
attach_paths = [str(p).strip() for p in attach_paths_in if str(p).strip()] if isinstance(attach_paths_in, list) else []
try:
from memory import MemoryPoint
vec = self.embedder.embed(content)
point = MemoryPoint(
id="", type=mem_type, title=title, content=content,
pinned=pinned, category=category, source="aria", tags=tags,
)
pid = self.store.upsert(point, vec)
# Anhaenge kopieren + Payload updaten
attach_errors: list[str] = []
if attach_paths:
import memory_attachments as mem_att
new_atts = []
for src in attach_paths:
try:
meta = mem_att.attach_from_path(pid, src)
new_atts.append(meta)
except ValueError as e:
attach_errors.append(f"{src}: {e}")
if new_atts:
from qdrant_client.http import models as qm
from memory.vector_store import COLLECTION
import datetime as _dt
now = _dt.datetime.now(_dt.timezone.utc).isoformat()
current = self.store.get(pid)
current.attachments = (current.attachments or []) + new_atts
current.updated_at = now
self.store.client.set_payload(
collection_name=COLLECTION,
payload=current.to_payload() | {"updated_at": now},
points=[pid],
)
saved = self.store.get(pid)
self._pending_events.append({
"type": "memory_saved",
"action": "created",
"memory": {
"id": saved.id, "type": saved.type, "title": saved.title,
"content_preview": (saved.content or "")[:140],
"category": saved.category, "pinned": saved.pinned,
"attachments": saved.attachments or [],
},
})
n_att = len(saved.attachments or [])
msg = (f"OK — Memory '{title}' gespeichert "
f"(type={mem_type}, pinned={pinned}, id={saved.id[:8]}"
+ (f", {n_att} Anhang/Anhaenge" if n_att else "") + ").")
if attach_errors:
msg += "\nHinweis: nicht alle Anhaenge konnten kopiert werden:\n - " + "\n - ".join(attach_errors)
return msg
except Exception as e:
logger.exception("memory_save fehlgeschlagen")
return f"FEHLER beim Speichern: {e}"
return f"Unbekanntes Tool: {name}"
except Exception as exc:
logger.exception("Tool '%s' fehlgeschlagen", name)
+1 -156
View File
@@ -23,7 +23,7 @@ from typing import List, Optional
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, UploadFile, File
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request
from fastapi.responses import Response
from pydantic import BaseModel, Field
@@ -114,10 +114,6 @@ class MemoryIn(BaseModel):
source: str = "manual"
tags: List[str] = Field(default_factory=list)
conversation_id: Optional[str] = None
# Vorhandene Anhang-Metadaten beim Save mitgeben (i.d.R. werden Anhaenge
# nach dem Save via /memory/{id}/attachments hinzugefuegt — hier eher fuer
# Bootstrap-Import/Restore-Faelle relevant).
attachments: List[dict] = Field(default_factory=list)
class MemoryUpdate(BaseModel):
@@ -141,19 +137,12 @@ class MemoryOut(BaseModel):
updated_at: str
conversation_id: Optional[str] = None
score: Optional[float] = None
attachments: List[dict] = Field(default_factory=list)
@classmethod
def from_point(cls, p: MemoryPoint) -> "MemoryOut":
return cls(**p.__dict__)
class AttachmentUploadBody(BaseModel):
"""Base64-Upload via JSON — Diagnostic schickt Files so."""
name: str
data_base64: str
# ─── Health ───────────────────────────────────────────────────────────
@app.get("/health")
@@ -167,16 +156,6 @@ def health():
# ─── Memory-Endpoints ─────────────────────────────────────────────────
@app.get("/memory/get/{point_id}", response_model=MemoryOut)
def memory_get(point_id: str):
"""Einzelner Memory mit allen Feldern (inkl. Anhaengen).
Pfad-Prefix /memory/get/ vermeidet Konflikt mit /memory/list, /memory/save etc."""
m = store().get(point_id)
if not m:
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
return MemoryOut.from_point(m)
@app.get("/memory/stats")
def memory_stats():
s = store()
@@ -202,23 +181,6 @@ def memory_pinned():
return [MemoryOut.from_point(p) for p in store().list_pinned()]
@app.get("/memory/search-text", response_model=List[MemoryOut])
def memory_search_text(
q: str,
k: int = 50,
type: Optional[str] = None,
include_pinned: bool = True,
):
"""Volltext-Substring-Suche (case-insensitive) ueber Title + Content +
Category + Tags. Findet exakte Begriffe — z.B. 'auto' matched 'Stefans Auto'.
Im Gegensatz zu /memory/search (semantic) keine 'klingt aehnlich'-Treffer."""
points = store().search_text(
q, k=k, type_filter=type,
exclude_pinned=not include_pinned,
)
return [MemoryOut.from_point(p) for p in points]
@app.get("/memory/search", response_model=List[MemoryOut])
def memory_search(
q: str,
@@ -252,7 +214,6 @@ def memory_save(body: MemoryIn):
source=body.source,
tags=body.tags,
conversation_id=body.conversation_id,
attachments=body.attachments or [],
)
pid = s.upsert(point, vec)
saved = s.get(pid)
@@ -301,125 +262,9 @@ def memory_delete(point_id: str):
if not s.get(point_id):
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
s.delete(point_id)
# Anhaenge mit-loeschen damit nichts verwaist
try:
import memory_attachments as mem_att
n = mem_att.delete_all(point_id)
if n:
logger.info("Memory %s + %d Anhaenge geloescht", point_id, n)
except Exception as exc:
logger.warning("Anhang-Cleanup fuer %s fehlgeschlagen: %s", point_id, exc)
return {"deleted": point_id}
# ─── Memory-Anhaenge ──────────────────────────────────────────────────
@app.get("/memory/{point_id}/attachments")
def memory_attachments_list(point_id: str):
"""Liste der Anhaenge zum Memory. Source-of-Truth ist das Payload
in der DB, aber wir mergen vorsichtshalber mit dem Filesystem-Stand
(falls ein Upload-Restart zwischendrin schiefging)."""
import memory_attachments as mem_att
s = store()
m = s.get(point_id)
if not m:
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
return {"memory_id": point_id, "attachments": mem_att.list_attachments(point_id)}
def _commit_attachment_meta(point_id: str, meta: dict) -> MemoryOut:
"""Shared-Helper: nach FS-Write das Payload um den neuen Anhang updaten.
Duplikat-Name wird ersetzt, sonst hinten dran."""
s = store()
m = s.get(point_id)
if not m:
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
atts = [a for a in (m.attachments or []) if a.get("name") != meta["name"]]
atts.append(meta)
m.attachments = atts
from memory.vector_store import COLLECTION
import datetime as _dt
m.updated_at = _dt.datetime.now(_dt.timezone.utc).isoformat()
s.client.set_payload(
collection_name=COLLECTION,
payload=m.to_payload() | {"updated_at": m.updated_at},
points=[point_id],
)
return MemoryOut.from_point(s.get(point_id))
@app.post("/memory/{point_id}/attachments", response_model=MemoryOut)
def memory_attachments_add(point_id: str, body: AttachmentUploadBody):
"""Anhang als Base64 hochladen — fuer Diagnostic + interne Tools.
Fuer grosse Files lieber multipart-Variante (/upload) nutzen,
Base64 sprengt schnell die Bash-ARG_MAX-Grenze beim curl."""
import memory_attachments as mem_att
if not store().get(point_id):
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
try:
meta = mem_att.save_from_base64(point_id, body.name, body.data_base64)
except ValueError as exc:
raise HTTPException(400, str(exc))
return _commit_attachment_meta(point_id, meta)
@app.post("/memory/{point_id}/attachments/upload", response_model=MemoryOut)
async def memory_attachments_upload(point_id: str, file: UploadFile = File(...)):
"""Multipart-Upload — Standard fuer Browser-FormData und curl -F.
Verwendung:
curl -F file=@foto.jpg "$ARIA_BRAIN_URL/memory/<id>/attachments/upload"
"""
import memory_attachments as mem_att
if not store().get(point_id):
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
data = await file.read()
try:
meta = mem_att.save_attachment(point_id, file.filename or "datei", data)
except ValueError as exc:
raise HTTPException(400, str(exc))
return _commit_attachment_meta(point_id, meta)
@app.delete("/memory/{point_id}/attachments/{filename}", response_model=MemoryOut)
def memory_attachments_delete(point_id: str, filename: str):
"""Einzelnen Anhang loeschen (FS + Payload-Eintrag)."""
import memory_attachments as mem_att
s = store()
m = s.get(point_id)
if not m:
raise HTTPException(404, f"Memory {point_id} nicht gefunden")
removed_fs = mem_att.delete_attachment(point_id, filename)
safe = filename # Cleanup synchron mit FS — Payload-Match per name
atts = [a for a in (m.attachments or []) if a.get("name") not in (filename, safe)]
m.attachments = atts
from qdrant_client.http import models as qm
from memory.vector_store import COLLECTION
import datetime as _dt
m.updated_at = _dt.datetime.now(_dt.timezone.utc).isoformat()
s.client.set_payload(
collection_name=COLLECTION,
payload=m.to_payload() | {"updated_at": m.updated_at},
points=[point_id],
)
if not removed_fs and not atts:
# weder im FS noch im Payload war was — Anhang existierte nicht
raise HTTPException(404, f"Anhang {filename} nicht gefunden")
return MemoryOut.from_point(s.get(point_id))
@app.get("/memory/{point_id}/attachments/{filename}")
def memory_attachments_get(point_id: str, filename: str):
"""Liefert die Bytes eines Anhangs. Diagnostic-Server kann das
durchproxien zur Vorschau/Download in der UI."""
import memory_attachments as mem_att
import mimetypes as _mt
data = mem_att.read_bytes(point_id, filename)
if data is None:
raise HTTPException(404, f"Anhang {filename} nicht gefunden")
mime = _mt.guess_type(filename)[0] or "application/octet-stream"
return Response(content=data, media_type=mime)
# ─── Migration aus brain-import/ ──────────────────────────────────────
IMPORT_DIR = os.environ.get("IMPORT_DIR", "/import")
-60
View File
@@ -60,11 +60,6 @@ class MemoryPoint:
updated_at: str = ""
conversation_id: Optional[str] = None
score: Optional[float] = None # nur bei Search gesetzt
# Anhaenge: Liste von Dicts {name, mime, size, path} — Dateien liegen
# physisch unter /shared/memory-attachments/<memory-id>/<name>.
# Hier in der DB nur die Metadaten, damit die Suche/Anzeige sie kennt
# ohne Filesystem zu pruefen.
attachments: List[dict] = field(default_factory=list)
def to_payload(self) -> dict:
p = {
@@ -77,7 +72,6 @@ class MemoryPoint:
"tags": self.tags,
"created_at": self.created_at,
"updated_at": self.updated_at,
"attachments": self.attachments,
}
if self.conversation_id:
p["conversation_id"] = self.conversation_id
@@ -98,7 +92,6 @@ class MemoryPoint:
created_at=payload.get("created_at", ""),
updated_at=payload.get("updated_at", ""),
conversation_id=payload.get("conversation_id"),
attachments=payload.get("attachments", []) or [],
score=getattr(point, "score", None),
)
@@ -220,56 +213,3 @@ class VectorStore:
def count(self) -> int:
return self.client.count(collection_name=COLLECTION, exact=True).count
def search_text(
self,
query: str,
k: int = 20,
type_filter: Optional[str] = None,
exclude_pinned: bool = False,
) -> List[MemoryPoint]:
"""Volltext-Substring-Suche (case-insensitive) ueber Title +
Content + Category + Tags. Im Gegensatz zu search() ist das KEIN
Semantic-Match — nur exakte Wort-/Teilwort-Treffer.
Full-Scan ueber alle (gefilteren) Punkte. Bei der erwarteten
Groessenordnung (< 1000) unkritisch."""
q = (query or "").strip().lower()
if not q:
return []
must = []
must_not = []
if type_filter:
must.append(qm.FieldCondition(key="type", match=qm.MatchValue(value=type_filter)))
if exclude_pinned:
must_not.append(qm.FieldCondition(key="pinned", match=qm.MatchValue(value=True)))
flt = qm.Filter(must=must or None, must_not=must_not or None) if (must or must_not) else None
matches: List[MemoryPoint] = []
offset = None
while True:
points, offset = self.client.scroll(
collection_name=COLLECTION,
scroll_filter=flt,
limit=200,
offset=offset,
with_payload=True,
with_vectors=False,
)
for p in points:
payload = p.payload or {}
tags = payload.get("tags")
tags_str = " ".join(tags) if isinstance(tags, list) else ""
haystack = " ".join([
str(payload.get("title", "")),
str(payload.get("content", "")),
str(payload.get("category", "")),
tags_str,
]).lower()
if q in haystack:
matches.append(MemoryPoint.from_qdrant(p))
if len(matches) >= k:
return matches
if not offset:
break
return matches
-172
View File
@@ -1,172 +0,0 @@
"""
Anhaenge fuer Memory-Eintraege.
Storage-Layout:
/shared/memory-attachments/<memory-id>/<original-name>
Eine flache Ordnerstruktur pro Memory — bei Memory-Delete loescht main.py
das ganze Verzeichnis. Anhang-Metadaten (name, mime, size, path) liegen
zusaetzlich im Qdrant-Payload des Memory-Punkts damit die Listen/Suche
sie ohne Filesystem-Lookup zeigen kann.
Anhaenge sind erstmal nur ueber die Diagnostic-UI hochladbar — ARIA
selbst hat in Stufe A kein Tool zum Upload.
"""
from __future__ import annotations
import base64
import logging
import mimetypes
import os
import re
import shutil
from pathlib import Path
from typing import List, Optional
logger = logging.getLogger(__name__)
ROOT = Path(os.environ.get("MEMORY_ATTACHMENTS_DIR", "/shared/memory-attachments"))
MAX_BYTES = int(os.environ.get("MEMORY_ATTACHMENT_MAX_BYTES", str(20 * 1024 * 1024))) # 20 MB
SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._\-]")
def _safe_filename(name: str) -> str:
"""Macht aus einem User-Namen einen filesystem-sicheren String —
zerlegt Pfadteile, schneidet Sonderzeichen weg, kuerzt auf 120 Zeichen."""
base = Path(name).name or "datei"
base = SAFE_NAME_RE.sub("_", base).strip("._-") or "datei"
return base[:120]
def memory_dir(memory_id: str) -> Path:
return ROOT / memory_id
def list_attachments(memory_id: str) -> List[dict]:
"""Liest die Anhaenge fuer eine Memory aus dem Filesystem.
Returns [{name, mime, size, path}, ...] — leer wenn nichts da.
Source of Truth ist Qdrant-Payload; diese Funktion ist nur fuer
Diagnostic-Endpoints wenn Stefan direkt das FS prueft."""
d = memory_dir(memory_id)
if not d.is_dir():
return []
out = []
for f in sorted(d.iterdir()):
if not f.is_file():
continue
out.append(_file_meta(memory_id, f))
return out
def _file_meta(memory_id: str, f: Path) -> dict:
try:
size = f.stat().st_size
except Exception:
size = 0
mime = mimetypes.guess_type(f.name)[0] or "application/octet-stream"
return {
"name": f.name,
"mime": mime,
"size": size,
"path": str(f), # absoluter Pfad im Container
}
def save_attachment(memory_id: str, filename: str, data: bytes) -> dict:
"""Schreibt einen Anhang ins FS und gibt seine Metadaten zurueck.
Ueberschreibt eine bestehende Datei mit gleichem Namen."""
if not memory_id:
raise ValueError("memory_id ist Pflicht")
if len(data) > MAX_BYTES:
raise ValueError(f"Anhang zu gross ({len(data)} > {MAX_BYTES} Byte)")
safe = _safe_filename(filename)
d = memory_dir(memory_id)
d.mkdir(parents=True, exist_ok=True)
target = d / safe
target.write_bytes(data)
logger.info("[mem-att] %s -> %s (%d Byte)", memory_id, safe, len(data))
return _file_meta(memory_id, target)
def save_from_base64(memory_id: str, filename: str, b64: str) -> dict:
"""Convenience fuer Base64-Uploads (Diagnostic schickt Files so)."""
try:
data = base64.b64decode(b64, validate=False)
except Exception as exc:
raise ValueError(f"Base64-Decode fehlgeschlagen: {exc}") from exc
return save_attachment(memory_id, filename, data)
def delete_attachment(memory_id: str, filename: str) -> bool:
"""Loescht eine einzelne Anhang-Datei. Returns True wenn was weg ist."""
safe = _safe_filename(filename)
target = memory_dir(memory_id) / safe
if not target.is_file():
return False
try:
target.unlink()
logger.info("[mem-att] %s/%s geloescht", memory_id, safe)
return True
except Exception as exc:
logger.warning("[mem-att] Loeschen fehlgeschlagen: %s", exc)
return False
def delete_all(memory_id: str) -> int:
"""Loescht das komplette Memory-Verzeichnis. Wird beim Memory-Delete
in main.py gerufen damit nichts verwaist."""
d = memory_dir(memory_id)
if not d.is_dir():
return 0
count = sum(1 for _ in d.iterdir() if _.is_file())
try:
shutil.rmtree(d)
logger.info("[mem-att] %s komplett entfernt (%d Files)", memory_id, count)
except Exception as exc:
logger.warning("[mem-att] rmtree fehlgeschlagen: %s", exc)
return count
def read_bytes(memory_id: str, filename: str) -> Optional[bytes]:
"""Liefert die rohen Bytes einer Datei zurueck — fuer Download/Serve."""
safe = _safe_filename(filename)
target = memory_dir(memory_id) / safe
if not target.is_file():
return None
return target.read_bytes()
# /shared/ ist der einzig akzeptable Source-Pfad fuer attach_from_path —
# ARIA bekommt Files vom User immer in /shared/uploads, eigene Files
# generiert sie in /shared/uploads/ als File-Marker. Kein Zugriff auf
# /root, /etc, /tmp, ssh-Keys, etc.
ALLOWED_SOURCE_PREFIXES = ("/shared/uploads/", "/shared/memory-attachments/")
def attach_from_path(memory_id: str, source_path: str) -> dict:
"""Kopiert eine existierende Datei aus /shared/* in das Anhang-Verzeichnis
des Memories und gibt die neue Metadaten zurueck.
Verwendung: ARIA bekommt z.B. ein User-Bild als `/shared/uploads/aria_<id>.jpg`.
Statt das Bild dort liegen zu lassen (kein direkter Memory-Bezug), kopiert
sie es via `memory_save(..., attach_paths=[<src>])` ins Memory-Verzeichnis.
Pfadschutz: source_path MUSS unter /shared/ liegen — kein Zugriff auf
Root-FS, SSH-Keys etc.
"""
if not memory_id:
raise ValueError("memory_id ist Pflicht")
if not source_path or not isinstance(source_path, str):
raise ValueError("source_path leer")
if not any(source_path.startswith(p) for p in ALLOWED_SOURCE_PREFIXES):
raise ValueError(f"source_path muss unter {' oder '.join(ALLOWED_SOURCE_PREFIXES)} liegen")
src = Path(source_path)
if not src.is_file():
raise ValueError(f"Datei nicht gefunden: {source_path}")
size = src.stat().st_size
if size > MAX_BYTES:
raise ValueError(f"Datei zu gross ({size} > {MAX_BYTES} Byte)")
# Reuse save_attachment damit Filename-Sanitization + Logging konsistent
data = src.read_bytes()
return save_attachment(memory_id, src.name, data)
-47
View File
@@ -52,44 +52,6 @@ TYPE_HEADINGS = {
}
def _attachments_line(p: MemoryPoint) -> str:
"""Eine Zeile die ARIA verraet welche Dateien an einer Memory haengen.
Bilder/Files liegen physisch unter /shared/memory-attachments/<id>/<name>.
Multi-Modal-Hinweis: Claude Code's `Read`-Tool kann Bilder direkt
anschauen (PNG/JPG/GIF/WebP) — sie laufen dann durch das gleiche
Vision-Modell wie via Anthropic-Vision-API. Heisst: ARIA muss nur
`Read /shared/memory-attachments/<id>/foto.jpg` aufrufen und sieht
das Bild wirklich, ohne dass wir Multi-Modal-Messages durch den
Proxy schleusen muessen. Wir geben ihr den Hinweis in der Zeile mit.
"""
atts = getattr(p, "attachments", None) or []
if not atts:
return ""
base_dir = f"/shared/memory-attachments/{p.id}/" if p.id else ""
items = []
has_image = False
for a in atts:
if not isinstance(a, dict):
continue
name = a.get("name", "?")
mime = a.get("mime", "")
if mime.startswith("image/"):
has_image = True
size = a.get("size")
size_part = f", {size // 1024} KB" if isinstance(size, int) and size else ""
items.append(f"{name} ({mime}{size_part})")
if not items:
return ""
line = f"📎 Anhaenge: {', '.join(items)}"
if base_dir:
line += f" — Pfad: {base_dir}"
if has_image and base_dir:
line += (" — Bilder kannst du via `Read <pfad>` direkt ansehen "
"(Claude Code Read ist multi-modal-faehig)")
return line
def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
"""Baue den 'IMMER-im-Prompt'-Block aus pinned Punkten."""
grouped: dict[str, List[MemoryPoint]] = {}
@@ -107,9 +69,6 @@ def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
for p in items:
parts.append(f"### {p.title}")
parts.append(p.content.strip())
att_line = _attachments_line(p)
if att_line:
parts.append(att_line)
parts.append("")
# uebrige Types (falls jemand was anderes als pinned markiert)
@@ -118,9 +77,6 @@ def build_hot_memory_section(pinned: List[MemoryPoint]) -> str:
for p in items:
parts.append(f"### {p.title}")
parts.append(p.content.strip())
att_line = _attachments_line(p)
if att_line:
parts.append(att_line)
parts.append("")
return "\n".join(parts).strip()
@@ -135,9 +91,6 @@ def build_cold_memory_section(matches: List[MemoryPoint]) -> str:
score = f" [score={p.score:.2f}]" if p.score is not None else ""
lines.append(f"- **{p.title}**{score}")
lines.append(f" {p.content.strip()}")
att_line = _attachments_line(p)
if att_line:
lines.append(f" {att_line}")
return "\n".join(lines)
-86
View File
@@ -1376,17 +1376,6 @@ class ARIABridge:
})
logger.info("[brain] location_tracking Request: on=%s (%s)",
event.get("on"), event.get("reason", ""))
elif etype == "memory_saved":
# ARIA hat selber etwas in die Vector-DB gespeichert.
# Eigene Bubble in App + Diagnostic (gelb wie skill/trigger).
await self._send_to_rvs({
"type": "memory_saved",
"payload": event.get("memory", {}),
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
logger.info("[brain] ARIA hat eine Memory angelegt: %s (type=%s)",
event.get("memory", {}).get("title"),
event.get("memory", {}).get("type"))
# _process_core_response uebernimmt alles weitere:
# File-Marker extrahieren + broadcasten, NO_REPLY-Check, Chat-
@@ -1824,72 +1813,6 @@ class ARIABridge:
logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error"))
return
elif msg_type == "brain_request":
# Generischer RVS-Proxy fuer die Brain-HTTP-API.
# payload: {requestId, method, path, body?, bodyBase64?, contentType?}
# - method: GET | POST | PATCH | DELETE
# - path: z.B. "/memory/list" oder "/memory/get/<id>"
# - body: JSON-Objekt (wird als JSON encoded)
# - bodyBase64: rohe Bytes als Base64 (fuer Upload mit contentType)
# - contentType: default application/json
# Antwort als brain_response {requestId, status, json?, base64?}.
req_id = payload.get("requestId") or ""
method = (payload.get("method") or "GET").upper()
path = payload.get("path") or ""
if not req_id or not path or not path.startswith("/"):
logger.warning("[rvs] brain_request ungueltig: %r", payload)
return
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
url = brain_url.rstrip("/") + path
headers: dict[str, str] = {}
data: Optional[bytes] = None
ct = payload.get("contentType") or "application/json"
if payload.get("bodyBase64"):
try:
data = base64.b64decode(payload["bodyBase64"])
except Exception:
data = None
if data is not None:
headers["Content-Type"] = ct
elif payload.get("body") is not None:
data = json.dumps(payload["body"]).encode("utf-8")
headers["Content-Type"] = "application/json"
logger.info("[rvs] brain_request %s %s (%d Byte)", method, path, len(data or b""))
def _do_call():
try:
req = urllib.request.Request(url, data=data, method=method, headers=headers)
with urllib.request.urlopen(req, timeout=120) as r:
return r.status, r.read(), r.headers.get("Content-Type", "")
except urllib.error.HTTPError as e:
try:
body = e.read()
except Exception:
body = b""
return e.code, body, e.headers.get("Content-Type", "") if e.headers else ""
except Exception as exc:
return None, str(exc).encode("utf-8"), "text/plain"
status, body_bytes, response_ct = await asyncio.get_event_loop().run_in_executor(None, _do_call)
out: dict = {"requestId": req_id, "status": status or 0}
if response_ct and "json" in response_ct:
try:
out["json"] = json.loads(body_bytes.decode("utf-8", errors="ignore"))
except Exception:
out["text"] = body_bytes.decode("utf-8", errors="ignore")[:2000]
elif response_ct and "text" in response_ct:
out["text"] = body_bytes.decode("utf-8", errors="ignore")[:4000]
else:
# Binaer (z.B. attachment-download) → base64 zurueck
out["base64"] = base64.b64encode(body_bytes).decode("ascii")
out["contentType"] = response_ct or "application/octet-stream"
await self._send_to_rvs({
"type": "brain_response",
"payload": out,
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
return
elif msg_type == "file_list_request":
# App fragt die Liste aller /shared/uploads/-Dateien an.
logger.info("[rvs] file_list_request von App")
@@ -2712,15 +2635,6 @@ class ARIABridge:
},
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
elif etype == "memory_saved":
mem = event.get("memory", {})
if event.get("action"):
mem = {**mem, "action": event.get("action")}
await self._send_to_rvs({
"type": "memory_saved",
"payload": mem,
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
except Exception:
logger.exception("[trigger-fire] Side-Channel-Event %s fehlgeschlagen", etype)
+18 -412
View File
@@ -824,17 +824,11 @@
</div>
<div class="card" style="margin-bottom:8px;">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<input type="text" id="brain-search" placeholder="Suche (z.B. 'cessna' oder 'Stefan Persönlichkeit')..."
<input type="text" id="brain-search" placeholder="Semantische Suche (z.B. 'Stefan Persönlichkeit')..."
style="flex:1;min-width:200px;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;"
onkeydown="if(event.key==='Enter') runBrainSearch()">
<select id="brain-search-mode" onchange="if(document.getElementById('brain-search').value.trim()) runBrainSearch()"
title="Wortlich = exakter Substring-Match. Semantisch = 'klingt aehnlich' via Embeddings."
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="text" selected>📝 Wortlich</option>
<option value="semantic">🧠 Semantisch</option>
</select>
<button class="btn secondary" onclick="runBrainSearch()" style="padding:4px 12px;font-size:11px;">Suchen</button>
<select id="brain-filter-type" onchange="onBrainFiltersChanged()"
<select id="brain-filter-type" onchange="loadBrainMemoryList()"
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="">Alle Typen</option>
<option value="identity">Identität</option>
@@ -846,29 +840,14 @@
<option value="conversation">Konversation</option>
<option value="reminder">Reminder</option>
</select>
<select id="brain-filter-pinned" onchange="onBrainFiltersChanged()"
<select id="brain-filter-pinned" onchange="loadBrainMemoryList()"
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="all">Pinned + Cold</option>
<option value="pinned">📌 Nur Pinned</option>
<option value="cold">Nur Cold</option>
</select>
<button class="btn secondary" onclick="toggleAdvancedSearch()" id="btn-advanced-search" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Erweiterte Suche mit AND/OR-Verknuepfungen">⌃ Erweitert</button>
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 8px;font-size:11px;color:#8888AA;" title="Suche + Filter zurücksetzen"></button>
</div>
<div id="brain-advanced-panel" style="display:none;margin-top:10px;padding:10px;background:#080810;border:1px solid #1E1E2E;border-radius:6px;">
<div style="color:#8888AA;font-size:11px;margin-bottom:6px;">
Mehrere Begriffe mit AND/OR verknuepfen — Volltext-Substring, case-insensitive, links-nach-rechts ausgewertet.
</div>
<div id="adv-rows-container" style="display:flex;flex-direction:column;gap:6px;">
<!-- Reihen werden dynamisch via JS gerendert (renderAdvancedRows) -->
</div>
<div style="display:flex;gap:6px;margin-top:8px;align-items:center;flex-wrap:wrap;">
<button class="btn" onclick="runAdvancedSearch()" style="padding:4px 12px;font-size:11px;">Suchen</button>
<button class="btn secondary" onclick="addAdvancedRow()" style="padding:4px 10px;font-size:11px;" title="Weiteres Suchfeld hinzufuegen">+ Feld</button>
<button class="btn secondary" onclick="clearAdvancedSearch()" style="padding:4px 10px;font-size:11px;color:#8888AA;">Alle leeren</button>
<span style="color:#555570;font-size:10px;margin-left:auto;">Leere Felder werden ignoriert · Min. 1 Feld · ✕ entfernt ein Feld</span>
</div>
</div>
<div id="brain-search-info" style="margin-top:6px;font-size:10px;color:#8888AA;display:none;"></div>
</div>
<div class="card">
@@ -1043,26 +1022,6 @@
<input type="checkbox" id="memory-pinned">
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
</label>
<!-- Anhaenge — nur bei Edit (vorhandene ID) sichtbar -->
<div id="memory-attachments-block" style="display:none;margin-top:14px;padding-top:10px;border-top:1px solid #1E1E2E;">
<label style="display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#8888AA;margin-bottom:6px;">
<span>📎 Anhaenge</span>
<span style="color:#555570;font-size:10px;">max 20 MB pro Datei</span>
</label>
<div id="memory-attachments-list" style="display:flex;flex-direction:column;gap:4px;margin-bottom:6px;font-size:12px;color:#555570;"></div>
<div style="display:flex;gap:6px;align-items:center;">
<label class="btn secondary" style="padding:4px 10px;font-size:11px;cursor:pointer;margin:0;">
⬆ Datei waehlen
<input type="file" id="memory-attachment-input" multiple style="display:none;" onchange="uploadMemoryAttachments(this.files)">
</label>
<span id="memory-attachment-status" style="font-size:11px;color:#555570;"></span>
</div>
</div>
<div id="memory-attachments-hint" style="display:none;margin-top:10px;padding:6px 8px;background:#0D0D1A;border-radius:4px;color:#555570;font-size:11px;">
📎 Anhaenge kannst du nach dem Speichern hinzufuegen (brauchen eine Memory-ID).
</div>
<div id="memory-modal-error" style="color:#FF6B6B;font-size:11px;margin-top:10px;display:none;"></div>
</div>
<div class="modal-footer" style="padding:10px 16px;border-top:1px solid #1E1E2E;display:flex;justify-content:flex-end;gap:8px;">
@@ -1403,14 +1362,6 @@
}
return;
}
if (msg.type === 'memory_saved') {
addMemorySavedBubble(msg.payload || {});
// Falls Gehirn-Tab offen: refreshen
if (document.getElementById('tab-brain') && document.getElementById('tab-brain').classList.contains('visible')) {
loadBrainMemoryList();
}
return;
}
if (msg.type === 'chat_delta') { return; }
if (msg.type === 'chat_error') {
addChat('error', msg.error, 'chat:error');
@@ -2004,39 +1955,6 @@
}
}
/** ARIA hat eine Memory in die Qdrant-DB gespeichert — als Bubble anzeigen. */
function addMemorySavedBubble(memory) {
const title = memory.title || '(ohne Titel)';
const type = memory.type || 'fact';
const cat = memory.category || '';
const pinned = !!memory.pinned;
const preview = memory.content_preview || '';
const typeLabel = (typeof BRAIN_TYPE_LABELS !== 'undefined' && BRAIN_TYPE_LABELS[type]) || type;
const pinBadge = pinned ? '<span style="color:#FFD60A;font-size:11px;margin-left:6px;">📌 pinned</span>' : '';
const catBadge = cat ? ` <span style="color:#555570;font-size:10px;">[${escapeHtml(cat)}]</span>` : '';
const html = `
<div style="font-weight:bold;color:#FFD60A;">🧠 ARIA hat etwas gemerkt</div>
<div style="margin-top:4px;color:#E0E0F0;">
<strong>${escapeHtml(title)}</strong>
<span style="color:#8888AA;font-size:11px;margin-left:6px;">(${escapeHtml(typeLabel)})</span>
${pinBadge}${catBadge}
</div>
${preview ? `<div style="color:#8888AA;font-size:12px;margin-top:2px;">${escapeHtml(preview)}${preview.length >= 140 ? '…' : ''}</div>` : ''}
<div class="meta">
ARIA-Memory — ${new Date().toLocaleTimeString('de-DE')} ·
<a href="#" onclick="event.preventDefault();switchMainTab('brain');" style="color:#FFD60A;">im Gehirn-Tab ansehen</a>
</div>`;
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = document.createElement('div');
el.className = 'chat-msg received';
el.style.borderLeft = '3px solid #FFD60A';
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
}
}
/** Wenn der Server file_deleted broadcastet: alle Bubbles mit
diesem serverPath rerendern als "geloescht" markieren. */
function markFileDeletedInChat(serverPath) {
@@ -3527,192 +3445,6 @@
const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all';
const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none';
brainSearchIds = null;
clearAdvancedSearch();
}
function toggleAdvancedSearch() {
const panel = document.getElementById('brain-advanced-panel');
const btn = document.getElementById('btn-advanced-search');
if (!panel) return;
const open = panel.style.display !== 'none';
panel.style.display = open ? 'none' : 'block';
if (btn) btn.textContent = open ? '⌃ Erweitert' : '⌄ Einklappen';
if (!open) ensureAdvancedRows();
}
// Dynamische Such-Reihen-Struktur:
// advRows = [{term, op}, ...] — die erste Reihe hat op=null,
// jede weitere bekommt einen UND/ODER-Selektor links und einen ✕ rechts.
let advRows = [{ term: '', op: null }];
function ensureAdvancedRows() {
if (!advRows.length) advRows = [{ term: '', op: null }];
renderAdvancedRows();
}
function addAdvancedRow() {
// Vor dem Re-render aktuelle Werte aus DOM uebernehmen damit nichts verloren geht
syncAdvancedRowsFromDOM();
advRows.push({ term: '', op: 'AND' });
renderAdvancedRows();
// Fokus auf das neue Feld
const last = document.querySelector(`#adv-rows-container .adv-row:last-child input.adv-term`);
if (last) last.focus();
}
function removeAdvancedRow(idx) {
syncAdvancedRowsFromDOM();
if (advRows.length <= 1) return; // erste bleibt
advRows.splice(idx, 1);
// Erste Reihe hat immer op=null
if (advRows[0]) advRows[0].op = null;
renderAdvancedRows();
}
function syncAdvancedRowsFromDOM() {
const rows = document.querySelectorAll('#adv-rows-container .adv-row');
const next = [];
rows.forEach((row, i) => {
const term = (row.querySelector('input.adv-term')?.value || '');
const op = i === 0 ? null : (row.querySelector('select.adv-op')?.value || 'AND');
next.push({ term, op });
});
if (next.length) advRows = next;
}
function renderAdvancedRows() {
const container = document.getElementById('adv-rows-container');
if (!container) return;
const inputStyle = 'flex:1;min-width:0;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:12px;';
const selectStyle = 'background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;width:70px;';
container.innerHTML = advRows.map((r, i) => {
const ph = i === 0 ? 'z.B. flugzeug' : 'z.B. cessna';
const term = (r.term || '').replace(/"/g, '&quot;');
if (i === 0) {
return `<div class="adv-row" style="display:flex;gap:6px;align-items:center;">
<span style="width:70px;color:#555570;font-size:11px;text-align:center;">Start</span>
<input type="text" class="adv-term" placeholder="${ph}" value="${term}" style="${inputStyle}">
<span style="width:24px;"></span>
</div>`;
}
const op = r.op || 'AND';
return `<div class="adv-row" style="display:flex;gap:6px;align-items:center;">
<select class="adv-op" style="${selectStyle}">
<option value="AND"${op === 'AND' ? ' selected' : ''}>UND</option>
<option value="OR"${op === 'OR' ? ' selected' : ''}>ODER</option>
</select>
<input type="text" class="adv-term" placeholder="${ph}" value="${term}" style="${inputStyle}">
<button class="btn secondary" onclick="removeAdvancedRow(${i})" title="Diese Zeile entfernen" style="width:24px;height:24px;padding:0;line-height:20px;font-size:11px;color:#FF6B6B;"></button>
</div>`;
}).join('');
}
function clearAdvancedSearch() {
advRows = [{ term: '', op: null }];
renderAdvancedRows();
}
/** Mehrere Volltext-Suchen + Boolean-Kombination (links nach rechts).
* Backend bleibt simpel — wir machen N parallele search-text-Calls
* und kombinieren die ID-Mengen client-seitig per AND/OR. */
async function runAdvancedSearch() {
syncAdvancedRowsFromDOM();
const info = document.getElementById('brain-search-info');
// Nur Reihen mit Inhalt einsammeln. Die erste belegte Reihe wird zum
// Start-Term (op=null), egal an welchem Index sie ursprünglich war.
const active = [];
for (const r of advRows) {
const t = (r.term || '').trim();
if (!t) continue;
active.push({ term: t, op: active.length === 0 ? null : (r.op || 'AND') });
}
if (active.length === 0) {
if (info) info.style.display = 'none';
loadBrainMemoryList();
return;
}
const typeFilter = document.getElementById('brain-filter-type').value;
const baseParams = { k: '500', include_pinned: 'true' };
if (typeFilter) baseParams.type = typeFilter;
try {
// Pro Begriff einmal Backend fragen, dann Map<id, memory> + Set<id>
const sets = [];
for (const a of active) {
const params = new URLSearchParams({ ...baseParams, q: a.term });
const r = await fetch('/api/brain/memory/search-text?' + params.toString());
if (!r.ok) throw new Error('HTTP ' + r.status);
const hits = await r.json();
hits.forEach(m => { brainMemoryCache[m.id] = m; });
sets.push(new Set(hits.map(m => m.id)));
}
// Links-nach-rechts kombinieren mit den Operatoren
let combined = sets[0];
for (let i = 1; i < sets.length; i++) {
const op = active[i].op;
if (op === 'AND') {
combined = new Set([...combined].filter(id => sets[i].has(id)));
} else {
combined = new Set([...combined, ...sets[i]]);
}
}
let hits = Array.from(combined).map(id => brainMemoryCache[id]).filter(Boolean);
const totalHits = hits.length;
hits = applyPinnedFilter(hits);
brainSearchIds = hits.map(m => m.id);
const desc = active.map((a, i) => i === 0 ? `"${a.term}"` : ` ${a.op} "${a.term}"`).join('');
const pinnedFilter = document.getElementById('brain-filter-pinned')?.value || 'all';
const pinnedLabel = pinnedFilter === 'pinned' ? ' · 📌 nur pinned'
: pinnedFilter === 'cold' ? ' · nur cold'
: '';
if (info) {
info.style.display = 'block';
const filterDesc = (typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') + pinnedLabel;
if (hits.length === 0) {
const extra = totalHits > 0 ? ` (${totalHits} Treffer ohne Pinned-Filter)` : '';
info.innerHTML = `🔍 Keine Treffer fuer ${escapeHtml(desc)}${filterDesc}${extra} · 📝 wortlich, Boolean-Kombi`;
} else {
info.innerHTML = `🔍 ${hits.length} Treffer fuer ${escapeHtml(desc)}${filterDesc} · 📝 wortlich, Boolean-Kombi`;
}
}
renderBrainList(hits, true);
} catch (e) {
if (info) {
info.style.display = 'block';
info.innerHTML = `🔴 Erweiterte Suche fehlgeschlagen: ${escapeHtml(e.message)}`;
}
}
}
/** True wenn aktuell eine Search-Ansicht aktiv ist (Single oder Advanced).
* Wird vom Pinned/Type-Filter-onchange genutzt um statt loadBrainMemoryList
* die Suche neu auszufuehren — damit Filter auch bei aktiver Suche greifen. */
function brainSearchActive() {
const q = (document.getElementById('brain-search')?.value || '').trim();
if (q) return 'single';
const hasAdv = (advRows || []).some(r => (r.term || '').trim());
return hasAdv ? 'advanced' : null;
}
/** Wird vom Type+Pinned-Dropdown onchange gerufen. Bei aktiver Suche
* re-search ausfuehren, sonst Liste neu laden. */
function onBrainFiltersChanged() {
const which = brainSearchActive();
if (which === 'single') runBrainSearch();
else if (which === 'advanced') runAdvancedSearch();
else loadBrainMemoryList();
}
/** Filtert eine Liste von Memories nach dem pinned-Dropdown-Wert.
* 'all' = alles durchlassen, 'pinned' = nur pinned, 'cold' = nur cold. */
function applyPinnedFilter(items) {
const v = document.getElementById('brain-filter-pinned')?.value || 'all';
if (v === 'pinned') return items.filter(m => m.pinned);
if (v === 'cold') return items.filter(m => !m.pinned);
return items;
}
async function runBrainSearch() {
@@ -3725,43 +3457,27 @@
return;
}
const typeFilter = document.getElementById('brain-filter-type').value;
const pinnedFilter = document.getElementById('brain-filter-pinned')?.value || 'all';
const mode = (document.getElementById('brain-search-mode')?.value) || 'text';
let url, modeLabel;
if (mode === 'semantic') {
// Embedder-basiert, mit Score-Threshold gegen Rauschen
const params = new URLSearchParams({ q, k: '20', include_pinned: 'true', score_threshold: '0.30' });
if (typeFilter) params.set('type', typeFilter);
url = '/api/brain/memory/search?' + params.toString();
modeLabel = '🧠 semantisch (Score ≥ 0.30)';
} else {
// Volltext-Substring (case-insensitive) — findet exakte Begriffe
const params = new URLSearchParams({ q, k: '100', include_pinned: 'true' });
if (typeFilter) params.set('type', typeFilter);
url = '/api/brain/memory/search-text?' + params.toString();
modeLabel = '📝 wortlich (Substring)';
}
// k=10 + Score-Threshold im Backend (0.30) → nur relevante Treffer.
// Frueher k=20 ohne Threshold: bei kleiner DB landete fast alles
// als "Treffer", egal wie unaehnlich.
const params = new URLSearchParams({ q, k: '10', include_pinned: 'true', score_threshold: '0.30' });
if (typeFilter) params.set('type', typeFilter);
try {
const r = await fetch(url);
const r = await fetch('/api/brain/memory/search?' + params.toString());
if (!r.ok) throw new Error('HTTP ' + r.status);
let hits = await r.json();
const hits = await r.json();
hits.forEach(m => { brainMemoryCache[m.id] = m; });
// Pinned-Filter clientseitig anwenden — Backend kennt nur include_pinned
// (all-or-none), wir brauchen aber feiner "nur pinned" / "nur cold".
const totalHits = hits.length;
hits = applyPinnedFilter(hits);
brainSearchIds = hits.map(m => m.id);
const pinnedLabel = pinnedFilter === 'pinned' ? ' · 📌 nur pinned'
: pinnedFilter === 'cold' ? ' · nur cold'
: '';
if (info) {
info.style.display = 'block';
const filterDesc = (typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') + pinnedLabel;
if (hits.length === 0) {
const extra = totalHits > 0 ? ` (${totalHits} Treffer ohne Pinned-Filter)` : '';
info.innerHTML = `🔍 Keine Treffer für "${escapeHtml(q)}"${filterDesc}${extra} · ${modeLabel}.`;
info.innerHTML = `🔍 Keine relevanten Treffer für "${escapeHtml(q)}"` +
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
` (Score < 0.30). Versuche andere Begriffe oder klicke das rechts um die Suche zu schliessen.`;
} else {
info.innerHTML = `🔍 ${hits.length} Treffer für "${escapeHtml(q)}"${filterDesc} · ${modeLabel}`;
info.innerHTML = `🔍 ${hits.length} Treffer für "${escapeHtml(q)}"` +
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
` · sortiert nach Aehnlichkeit (Score &ge; 0.30)`;
}
}
renderBrainList(hits, true);
@@ -3869,11 +3585,9 @@
const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' ');
const score = withScore && typeof m.score === 'number' ? `<span style="color:#FFD60A;font-size:10px;margin-left:6px;">${m.score.toFixed(2)}</span>` : '';
const typeBadge = withScore ? `<span style="color:#0096FF;font-size:10px;margin-right:6px;">${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}</span>` : '';
const attCount = Array.isArray(m.attachments) ? m.attachments.length : 0;
const attBadge = attCount > 0 ? `<span style="color:#34C759;font-size:10px;margin-left:6px;" title="${attCount} Anhang${attCount === 1 ? '' : ' / Anhaenge'}">📎${attCount}</span>` : '';
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}${attBadge}
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
</div>
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
@@ -4066,13 +3780,6 @@
const errEl = document.getElementById('memory-modal-error');
errEl.style.display = 'none';
const attBlock = document.getElementById('memory-attachments-block');
const attHint = document.getElementById('memory-attachments-hint');
const attStatus = document.getElementById('memory-attachment-status');
if (attStatus) attStatus.textContent = '';
const attInput = document.getElementById('memory-attachment-input');
if (attInput) attInput.value = '';
if (id && brainMemoryCache[id]) {
const m = brainMemoryCache[id];
titleEl.textContent = 'Memory bearbeiten';
@@ -4083,10 +3790,6 @@
document.getElementById('memory-category').value = m.category || '';
document.getElementById('memory-tags').value = (m.tags || []).join(', ');
document.getElementById('memory-pinned').checked = !!m.pinned;
// Anhang-Block sichtbar — Liste rendern
if (attBlock) attBlock.style.display = 'block';
if (attHint) attHint.style.display = 'none';
renderMemoryAttachmentsList(m.attachments || []);
} else {
titleEl.textContent = 'Neue Memory';
idEl.value = '';
@@ -4096,96 +3799,10 @@
document.getElementById('memory-category').value = '';
document.getElementById('memory-tags').value = '';
document.getElementById('memory-pinned').checked = false;
// Bei neuem Memory: nur Hinweis, dass Anhaenge nach Save gehen
if (attBlock) attBlock.style.display = 'none';
if (attHint) attHint.style.display = 'block';
}
modal.classList.add('open');
}
function renderMemoryAttachmentsList(atts) {
const el = document.getElementById('memory-attachments-list');
if (!el) return;
const id = document.getElementById('memory-edit-id').value;
if (!Array.isArray(atts) || atts.length === 0) {
el.innerHTML = '<div style="color:#555570;font-size:11px;font-style:italic;">(noch keine Anhaenge)</div>';
return;
}
el.innerHTML = atts.map(a => {
const name = escapeHtml(a.name || '?');
const mime = a.mime || 'application/octet-stream';
const size = a.size ? `${(a.size / 1024).toFixed(0)} KB` : '';
const isImage = mime.startsWith('image/');
const url = `/api/brain/memory/${encodeURIComponent(id)}/attachments/${encodeURIComponent(a.name)}`;
const preview = isImage
? `<img src="${url}" style="width:32px;height:32px;object-fit:cover;border-radius:4px;cursor:pointer;" onclick="openLightbox('image','${url}')">`
: `<span style="display:inline-block;width:32px;text-align:center;font-size:18px;">📄</span>`;
return `<div style="display:flex;align-items:center;gap:8px;padding:4px 6px;background:#0D0D1A;border-radius:4px;">
${preview}
<a href="${url}" target="_blank" style="flex:1;min-width:0;color:#E0E0F0;text-decoration:none;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${name}">${name}</a>
<span style="color:#555570;font-size:10px;flex-shrink:0;">${escapeHtml(mime)}, ${size}</span>
<button class="btn secondary" onclick="deleteMemoryAttachment('${encodeURIComponent(a.name)}')" title="Anhang loeschen" style="padding:2px 6px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;">🗑</button>
</div>`;
}).join('');
}
async function uploadMemoryAttachments(files) {
if (!files || !files.length) return;
const id = document.getElementById('memory-edit-id').value;
if (!id) return;
const status = document.getElementById('memory-attachment-status');
let lastResult = null;
let n = 0;
for (const file of files) {
if (status) status.textContent = `⏳ Lade ${file.name} (${(file.size/1024).toFixed(0)} KB)...`;
try {
const form = new FormData();
form.append('file', file, file.name);
const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/upload`, {
method: 'POST',
body: form,
});
if (!r.ok) {
const txt = await r.text();
throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200));
}
lastResult = await r.json();
n += 1;
} catch (e) {
if (status) status.textContent = `🔴 ${file.name}: ${e.message}`;
break;
}
}
if (lastResult) {
brainMemoryCache[id] = lastResult;
renderMemoryAttachmentsList(lastResult.attachments || []);
if (status) status.textContent = `✓ ${n} Anhang${n === 1 ? '' : '/Anhaenge'} hochgeladen`;
// Eingabe-File-List reset damit erneutes Anwaehlen derselben Datei feuert
const inp = document.getElementById('memory-attachment-input');
if (inp) inp.value = '';
}
}
async function deleteMemoryAttachment(filenameEncoded) {
const id = document.getElementById('memory-edit-id').value;
if (!id) return;
const name = decodeURIComponent(filenameEncoded);
if (!confirm(`Anhang "${name}" wirklich loeschen?`)) return;
try {
const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/${filenameEncoded}`, {
method: 'DELETE',
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const updated = await r.json();
brainMemoryCache[id] = updated;
renderMemoryAttachmentsList(updated.attachments || []);
const status = document.getElementById('memory-attachment-status');
if (status) status.textContent = `✓ "${name}" geloescht`;
} catch (e) {
alert('Loeschen fehlgeschlagen: ' + e.message);
}
}
function closeMemoryModal() {
document.getElementById('memory-modal').classList.remove('open');
}
@@ -4239,18 +3856,7 @@
try {
const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' });
if (!r.ok) throw new Error('HTTP ' + r.status);
// Lokalen Cache + Such-State bereinigen damit die Liste nicht den Geist
// des geloeschten Eintrags weiterzeigt.
delete brainMemoryCache[id];
if (Array.isArray(brainSearchIds)) {
brainSearchIds = brainSearchIds.filter(x => x !== id);
}
// Re-Render: bei aktiver Suche neu suchen (Filter respektieren),
// sonst die Vollliste neu vom Server holen.
const which = (typeof brainSearchActive === 'function') ? brainSearchActive() : null;
if (which === 'single') await runBrainSearch();
else if (which === 'advanced') await runAdvancedSearch();
else await loadBrainMemoryList();
loadBrainMemoryList();
loadBrainStatus();
} catch (e) {
alert('Löschen fehlgeschlagen: ' + e.message);
+1 -26
View File
@@ -617,26 +617,6 @@ function connectRVS(forcePlain) {
// Mode-Broadcast von der Bridge → an Browser-Clients weiterreichen
log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`);
broadcast({ type: "mode", payload: msg.payload });
} else if (msg.type === "agent_activity") {
// Bridge meldet "ARIA denkt/schreibt/tool" oder "idle" — an Browser
// weiterreichen, damit der Thinking-Indikator im Chat erscheint.
// Wenn gerade ein chat:final vorbei ist, unterdruecken wir trailing
// 'thinking'-Events (gleiches Schema wie alter OpenClaw-Pfad).
const activity = msg.payload?.activity || msg.activity || "idle";
if (activity !== "idle" && Date.now() - lastChatFinalAt < SETTLED_WINDOW_MS) {
// chat:final ist gerade durch — verstaubende thinking-Events ignorieren
} else {
broadcast({
type: "agent_activity",
activity,
tool: msg.payload?.tool || msg.tool || "",
});
}
} else if (msg.type === "memory_saved") {
// ARIA hat selber etwas in die Qdrant-DB gespeichert (via memory_save Tool).
const m = msg.payload || {};
log("info", "rvs", `ARIA-Memory gespeichert: "${m.title}" (type=${m.type}, pinned=${m.pinned})`);
broadcast({ type: "memory_saved", payload: m });
} else if (msg.type === "chat_message_deleted") {
// Bridge meldet: Bubble wurde aus chat_backup + Brain entfernt.
// An Browser-Clients weiterreichen damit sie die Bubble lokal entfernen.
@@ -1644,18 +1624,13 @@ const server = http.createServer((req, res) => {
// Reverse-Proxy zum aria-brain Container (intern auf 8080, nicht expose'd).
// Frontend ruft z.B. /api/brain/health → http://aria-brain:8080/health
const targetPath = req.url.replace(/^\/api\/brain/, "");
// Uploads brauchen laenger als die 30s default — Memory-Anhang-Endpoints
// koennen bis zu 20 MB tragen, plus chat/distill-Calls dauern manchmal
// mehr als eine Minute.
const isUpload = /\/attachments(\/upload)?$/.test(targetPath);
const timeout = isUpload ? 120000 : 60000;
const proxyReq = http.request({
host: "aria-brain",
port: 8080,
path: targetPath,
method: req.method,
headers: req.headers,
timeout,
timeout: 30000,
}, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
-8
View File
@@ -20,14 +20,6 @@ services:
- ./aria-data/ssh:/root/.ssh # SSH Keys fuer VM-Zugriff (aria-wohnung, rw fuer ARIA)
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
- ./proxy-patches:/proxy-patches:ro # Tool-Use-Adapter (ueberschreibt npm-Version, read-only)
# Claude Code's eingebautes Auto-Memory liegt in ~/.claude/projects/.
# Wir ueberlagern das mit tmpfs damit ARIA nicht parallel zu ARIAs eigener
# Qdrant-DB ein File-Memory aufbaut (war Auslöser fuer doppelte Truth-Source).
# Tmpfs ist beim Container-Start leer und wird beim Container-Recreate
# weggeworfen — Claude Code sieht keine alten Files mehr und das was sie
# ggf. neu schreibt landet nicht auf dem VM-Host.
tmpfs:
- /root/.claude/projects
environment:
- HOST=0.0.0.0
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
-33
View File
@@ -55,13 +55,6 @@ Wichtige Mechanismen:
### Bugs / Fixes
- [x] **Cold Memory Crosstalk** durch Score-Threshold im Brain-Agent: Bei kleiner DB lieferte Cold-Search ungefiltert Top-5, auch wenn alle Scores < 0.2 lagen — ARIA hat das als „relevante" Info in den System-Prompt bekommen und in die Antwort eingewoben. Beispiel: Frage „hab ich ein flugzeug?" → Cold-Top war „Firmenadresse" (Score 0.094, Embedder-Noise) → ARIA antwortete „Die Adresse aus meinem Gedaechtnis ist..." ohne dass User danach gefragt hatte. Fix: Konstante `COLD_SCORE_THRESHOLD=0.30` in `agent.py` an `store.search()` durchgereicht. Konsistent mit dem `/memory/search`-HTTP-Threshold und der Diagnostic-Suche
- [x] **Diagnostic: Pinned-/Type-Filter wirkt jetzt auch bei aktiver Suche**: Vorher ignorierten `runBrainSearch`/`runAdvancedSearch` die Filter-Dropdowns komplett; Dropdown-onchange rief `loadBrainMemoryList` und brach die Suche damit ab. Fix: `applyPinnedFilter` clientseitig nach Backend-Hit, `onBrainFiltersChanged` re-search bei aktiver Suche
- [x] **Diagnostic: Memory-Liste refresht nach Delete sofort**: vorher rendere `loadBrainMemoryList` bei aktiver Such-Ansicht aus `brainMemoryCache` → der gerade geloeschte Eintrag tauchte wieder auf. Fix: Cache + brainSearchIds nach Delete bereinigen + re-search statt list
- [x] **Diagnostic: „ARIA denkt..."-Indikator wieder im Chat-Fenster**: `agent_activity`-Events von RVS wurden vom Diagnostic-Server nicht an Browser durchgereicht. Fix: Relay analog zu `mode`/`voice_ready`, mit `SETTLED_WINDOW_MS`-Schutz gegen Trailing-Events nach `chat:final`
- [x] **Memory-Suche filtert Rauschen** (score_threshold im HTTP-Endpoint + kleineres k): Vorher k=20 ohne Threshold lieferte bei kleiner DB fast alles als Treffer, auch komplettes Rauschen (z.B. „banane" → 10 false positives mit Score 0.10-0.22). Fix: `score_threshold=0.30` als Query-Param am `/memory/search`-Endpoint + Diagnostic schickt jetzt `k=10` + Threshold, „Keine Treffer"-Box wenn alle unter Score
- [x] **Cessna-Beispiel aus System-Prompt raus**: in der `memory_save`-Tool-Description stand „z.B. 'Stefan hat eine Cessna'" als fact-Beispiel. ARIA hat das (korrekt!) korrekt eingeordnet als Beispiel-Text, aber Phantom-Wissen im Prompt ist suboptimal. Fix: durch generische Aufzaehlung (Vorlieben/Besitz/Orte/Termine/Personen) ersetzt
- [x] **Claude-Code-Auto-Memory abklemmen**: Claude Code CLI hat ein eingebautes Auto-Memory das Markdown-Files in `~/.claude/projects/<project>/memory/` schreibt. Weil das CLI als ARIAs LLM lief, hat sie da ueber Wochen ihre eigene Schatten-Wissensbasis aufgebaut (cessna, persoenlichkeit, projects) — komplett parallel zur Qdrant-DB. Fix: `tmpfs`-Mount ueber `/root/.claude/projects` im Proxy-Container. Claude Code sieht beim Spawn leeres `projects/`, schreibt sie was rein landet's nur im RAM, beim Container-Recreate weg. Stefans persoenliches `~/.claude/projects/` auf der VM bleibt unangetastet
- [x] **Trigger-Antworten landen jetzt im Chat** (App + Diagnostic + TTS): Wenn der Brain-Background-Loop einen Timer/Watcher feuert, ruft er `agent.chat()` direkt im eigenen Prozess. Die Antwort wurde nur ins Trigger-Log geschrieben — kein RVS-Broadcast, nichts sichtbar. Fix: Bridge hat jetzt einen kleinen asyncio HTTP-Listener auf Port 8090 (intern, nicht exposed). Brain pusht nach jedem Trigger-Feuer per `urllib.request.urlopen` an `http://aria-bridge:8090/internal/trigger-fired` mit `{reply, trigger_name, type, events}`. Bridge ruft `_handle_trigger_fired` → Side-Channel-Events (skill_created/trigger_created/location_tracking) + `_process_core_response` — exakt derselbe Pfad wie normale Chat-Antworten (Bubble + TTS + chat_backup)
- [x] **Tool-Use im Proxy durchgereicht** (claude-max-api-proxy): Der Proxy nahm das OpenAI-`tools`-Feld an, ignorierte es aber komplett — `openai-to-cli.js` wandelte nur `messages` zu einem String, `manager.js` rief `claude --print` ohne Tools. Claude Code nutzte ihre internen Tools (Bash, Read, ...) und „simulierte" Aktionen wie `sleep 120` statt `trigger_timer` zu rufen. Fix: zwei eigene Adapter-Files unter `proxy-patches/`, die zur Container-Startzeit ueber die npm-Version kopiert werden. `openai-to-cli.js` injiziert die `tools` als `<system>`-Block mit Schema-Beschreibungen und der Anweisung `<tool_call name="X">{json}</tool_call>` als Antwortformat zu verwenden; weiterhin verarbeitet sie `role=tool`-Messages als `<tool_result>`-Bloecke fuer den Loop-Replay. `cli-to-openai.js` parsed die `<tool_call>`-Bloecke aus dem Result-Text zurueck zu OpenAI `tool_calls` mit `finish_reason=tool_calls`. Mehrere Tool-Calls + Pre-Tool-Text werden korrekt aufgeteilt
- [x] **Timer "in 2 Minuten" wird wieder angelegt**: ARIA hatte keine Moeglichkeit die aktuelle Zeit zu kennen — kein Bash-Tool, kein Time-Tool, kein Timestamp im System-Prompt. Die Tool-Beschreibung von `trigger_timer` empfahl sogar `date -u -d '+10 minutes'` via Bash, aber Bash gab's nicht. Folge: LLM liess den Tool-Call entweder weg oder riet einen Cutoff-Zeitstempel (Vergangenheit) → Background-Loop feuerte beim naechsten 30s-Tick sofort statt in 2min. Fix: (1) `build_time_section()` in `prompts.py` injiziert UTC + lokale Europa/Berlin-Zeit als `## Aktuelle Zeit`-Block oben im System-Prompt. (2) `trigger_timer` akzeptiert jetzt `in_seconds` als Alternative zu `fires_at` — Server rechnet den absoluten Timestamp, ARIA muss nicht ISO-rechnen
@@ -287,32 +280,6 @@ Skills mit Tool-Use.
- [x] **Triggers-Block im System-Prompt**: aktive Trigger + verfuegbare Variablen + Funktionen werden bei jedem Chat-Turn injiziert, dazu Hinweis dass GPS-Watcher `request_location_tracking` mit-aufrufen sollen
- [x] **Aktuelle-Zeit-Block im System-Prompt**: UTC + lokale Europa/Berlin-Zeit (Sommer/Winter-Heuristik) wird bei jedem Chat-Turn oben mit-injiziert, damit Timer-fires_at und Watcher mit `hour_of_day` ueberhaupt sinnvoll sind. `trigger_timer` akzeptiert zusaetzlich `in_seconds` (Server rechnet) — ARIA muss bei relativen Angaben ('in 2 Minuten') nicht selbst ISO-rechnen
### Memory-System (Phase B Punkt 5+ Bonus)
- [x] **`memory_save`-Tool fuer ARIA**: ARIA kann selber neue Memories in die Qdrant-DB schreiben (vorher hat sie auf File-Memory ausweichen muessen weil kein Tool da war). Schema: `title`, `content`, `type` (identity/rule/preference/tool/skill/fact/conversation/reminder), optional `category`, `tags`, `pinned`. Tool-Description erklaert die Type-Wahl + sagt explizit „Du hast KEIN File-Memory mehr, schreibe nicht in `~/.claude/projects/...`". Side-Channel-Event `memory_saved` broadcastet via Bridge an App + Diagnostic — gelbe „🧠 ARIA hat etwas gemerkt"-Bubble, Auto-Refresh des Gehirn-Tabs falls offen
- [x] **Volltext-Suche im Gehirn** (`/memory/search-text`): Substring-Match (case-insensitive) ueber Title + Content + Category + Tags. Default in der Diagnostic-Suche, weil bei kleiner DB Semantic Search False-Positives ueberproduziert. Toggle „🧠 Semantisch" wechselt zu Embedder-Modus
- [x] **Advanced Search im Diagnostic-Gehirn-Tab**: aufklappbares Panel mit dynamisch erweiterbaren Suchfeldern (+ Feld Button) und UND/ODER-Operatoren zwischen ihnen. Backend-side bleibt simpel — pro Begriff einmal `/memory/search-text`, dann clientseitig per Set-Logik kombiniert. Pinned-/Type-Filter werden mit angewandt
- [x] **Mülltonne pro Chat-Bubble**: einzelne Nachrichten loeschbar (mit Confirm). Entfernt aus chat_backup.jsonl, Brain conversation.jsonl (rolling window) und allen Clients per RVS-Broadcast `chat_message_deleted`. Wichtig fuer ARIA: geloeschte Turns sind im naechsten Prompt nicht mehr im Window
- [x] **Druckansicht fuer Memories**: 📄-Button im Gehirn-Tab oeffnet eine fuer A4-Print optimierte Ansicht in neuem Tab — Strg+P → Als PDF speichern. Filter (Typ + Pinned) werden respektiert
- [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
### 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] **Backend-Endpoints**: GET/POST/DELETE `/memory/{id}/attachments[/...]`, plus Multipart-Upload-Variante `/upload` fuer Browser-FormData (Base64-Upload sprengt bei grossen Files Bash's ARG_MAX, multipart ist sauberer). Diagnostic-Proxy mit dynamischem Timeout (120s fuer /attachments, 60s sonst)
- [x] **Diagnostic-UI**: Memory-Modal hat Upload-Block (multiple File-Picker), Thumbnail-Vorschau bei Bildern + 📄-Icon bei Files, Klick auf Bild → Lightbox, 🗑 pro Anhang. Memory-Liste zeigt 📎N-Badge wenn N > 0 Anhaenge
- [x] **App-UI**: `memory_saved`-Bubble zeigt Anhaenge als Tap-Reihen. Tap → `file_request` ueber RVS → Bridge laedt + bei Bildern Vollbild-Modal, bei anderen Intent-Picker. `file_response`-Handler matched zusaetzlich `memorySaved.attachments[].path`
- [x] **System-Prompt-Integration**: `_attachments_line` in `prompts.py` haengt nach Hot/Cold-Memory-Eintraegen eine `📎 Anhaenge: foo.jpg (...) — Pfad: ...`-Zeile an. Bei `image/*` zusaetzlich Hinweis „Bilder kannst du via `Read <pfad>` direkt ansehen — Claude Code Read ist multi-modal-faehig"
- [x] **ARIA sieht Bilder echt** — Stufe E ohne Proxy-Patch: Claude Code's `Read`-Tool ist bereits multi-modal. ARIA ruft `Read /shared/memory-attachments/<id>/foto.jpg` → Vision-Modell beschreibt das Bild, ARIA antwortet mit den extrahierten Infos. End-to-End getestet mit Cessna-Foto: ARIA hat D-ECSW-Kennung aus dem Bild gelesen, F172-Variante erkannt (Reims-Aviation), EDWM-ICAO fuer Mariensiel selbst dazu kombiniert. **Persistent**: Bild bleibt am Memory, bei spaeteren Detail-Fragen („wie viele Fenster?") kann ARIA das Bild nochmal lesen ohne dass User es re-uploaden muss
- [x] **`memory_save` mit `attach_paths`** — ARIA kann beim Speichern selber Bilder anhaengen. Pfade aus `/shared/uploads/` (z.B. ein User-Foto aus dem Chat) werden serverseitig nach `/shared/memory-attachments/<id>/` kopiert. Pfadschutz auf Whitelist-Prefixes (kein Root-FS-Zugriff). Tool-Description weist explizit an: erst `Read <pfad>` (Vision-Beschreibung), dann `memory_save(content=<extrahierte Infos>, attach_paths=[<pfad>])` — End-to-End-Workflow in einer Tool-Call-Sequenz
### DB als Single Source of Truth
- [x] **`brain-import/` als Drop-Folder** statt aktive Saat: Inhalt komplett gitignored, nur `.gitkeep` + README im Repo. Stefan kippt MDs rein wenn er was migrieren will, klickt im Diagnostic „Migration aus brain-import/", fertig. Alte AGENT.md/BOOTSTRAP.md aus dem Repo geworfen (waren teils OpenClaw-Altlasten)
- [x] **DB-Aufraeumung**: 60 → 31 Eintraege durch Loeschen von 24 Dubletten (gleicher Title+Content unter verschiedenen IDs aus der initialen Migration) + 6 obsoleten facts (OpenClaw-Geschichte, Home-Partition-Snapshots etc.). Firmenadresse als einzige aktive `fact` behalten
- [x] **`.claude/aria-vm.env` Setup** fuer die Dev-Maschine: Claude Code auf Stefans Workstation erreicht das Brain-API ueber Diagnostic-Port 3001 via `ARIA_BRAIN_URL`. `.example` im Repo, echte Datei mit IP der VM gitignored. Damit kann Claude direkt curl gegen die DB machen ohne SSH-Tunnel
### Diagnostic / App Features (drumherum)
- [x] Datei-Manager (Diagnostic + App-Modal): /shared/uploads/ verwalten, Multi-Select + Select-All + Bulk-Download als ZIP + Bulk-Delete
-2
View File
@@ -26,11 +26,9 @@ const ALLOWED_TYPES = new Set([
"xtts_import_voice", "xtts_voice_imported",
"skill_created",
"trigger_created",
"memory_saved",
"location_update", "location_tracking",
"chat_history_request", "chat_history_response", "chat_cleared",
"delete_message_request", "chat_message_deleted",
"brain_request", "brain_response",
"file_delete_batch_request", "file_delete_batch_response",
"file_zip_request", "file_zip_response",
"xtts_delete_voice",
+1 -1
View File
@@ -4,7 +4,7 @@
# ════════════════════════════════════════════════
# RVS Verbindung (gleiche Daten wie auf der ARIA-VM)
RVS_HOST=mobil.hacker-net.de
RVS_HOST=example.com
RVS_PORT=444
RVS_TLS=true
RVS_TLS_FALLBACK=true