Lokal bekam ARIA starr die letzten 8 Nachrichten — alles davor war fuer das
lokale Modell weg. Neu (local_compact.py):
- Fenster nach Tokens: so viele der letzten Nachrichten, wie in ~75 % des
Kontextfensters der Karte passen (Systemprompt + Antwort abgezogen), max.
LOCAL_MAX_TURNS (12). Kontextfenster vom Cluster (Bridge /internal/llm-models).
- Was herausfaellt, fasst das lokale Modell im Hintergrund zum laufenden
Notizzettel zusammen (pro Projekt, /data/local_summaries.json); er geht als
"Bisheriger Verlauf" in den lokalen Systemprompt. Destillat ins Gedaechtnis
laeuft unveraendert weiter. Reset des Gespraechs loescht auch den Zettel.
- Bridge/local_llm: optionale laengere Frist fuer Hintergrund-Aufrufe
(Compacting 180 s statt der 30 s fuer schnelle Antworten).
Diagnostic: Info-Box zum Kontext erklaert Kuerzen / Compacting / Gedaechtnis
und was ARIA davon nutzt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RVS: die ARIA-App wird auch ohne client_hello erkannt, sobald sie eine
Nachricht schickt, die nur sie sendet (app_log mit platform android/ios,
location_update, stt_lease_request, update_download, vnc_open).
Bridge: "Voice-Config geladen" und "Voice-Config gespeichert" schrieben
den huggingfaceToken noch im Klartext ins Log — jetzt ***.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Einstellungen, zwischen Runtime-Konfiguration und App-Onboarding: alle
Verbindungen im Raum des konfigurierten Tokens — Art, Name, Details,
Adresse, verbunden seit, zuletzt aktiv.
RVS merkt sich pro Verbindung, wer sie ist: aus Nachrichten, die ohnehin
fliessen (sat_hello, host_hello, worker_hello, update_check der App,
llm_cluster_*), oder aus dem neuen client_hello (Bridge, Diagnostic, App
mit Geraetename). Adresse aus X-Forwarded-For hinter einem Reverse-Proxy.
room_clients_query liefert NUR den eigenen Raum; room_clients von Clients
wird nicht weitergereicht. Typen in der Allow-List ergaenzt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Bridge schrieb beim Config-Broadcast den kompletten Payload ins Log,
inklusive huggingfaceToken. Jetzt werden Werte von Schluesseln wie token,
secret, passw, apikey, credential als *** ausgegeben (Zaehler wie maxTokens
bleiben sichtbar). Dasselbe fuer das Debug-Log der RVS-Nachrichten in der
Diagnostic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Programme sollen nicht Box und Karte aussuchen muessen. Die ARIA-Bridge kennt
alle Boxen samt Karten-Belegung und verteilt (bridge/llm_cluster.py):
- Auswahl: unter allen online Boxen mit dem Modell die mit den wenigsten
laufenden Anfragen, bei Gleichstand reihum. Eine Karte / alles frei →
alles dorthin ("verteilen, wenn noetig").
- Ausfall: geht eine Box waehrend der Anfrage offline (kein Ping mehr), wird
sie EINMAL an eine andere Box mit demselben Modell gegeben. Modell-Fehler
(z.B. Kontext zu lang) werden nicht wiederholt — waeren ueberall gleich.
- Fehler kommen als Fehler (ok=false + errorCode), nie als Antwort-Text: ein
Programm wuerde ihn sonst als Aussage der KI in seinen Verlauf schreiben —
so ist bei ARIA schon mal ein ganzer Chat kaputtgegangen.
RVS: llm_cluster_models_request → llm_cluster_models (jedes Modell EINMAL,
ctx = kleinstes Kontextfenster unter seinen Karten), llm_cluster_request →
llm_cluster_response. In der RVS-Allow-List freigegeben.
ARIA nutzt denselben Router (mit ihrer 30-s-Frist fuer den lokalen
Schnellweg) — vorher Round-Robin ohne Ausfall-Ersatz, und errorCode ging
verloren.
xtts/openai_rvs_wrapper: fuer Programme, die nur "OpenAI-kompatibel" koennen
(OpenHands & Co.). Kleiner Container NEBEN dem Programm, fuer das der Cluster
dann http://127.0.0.1:8099/v1 ist; braucht nur eine ausgehende Verbindung zum
RVS. /v1/models (jedes Modell einmal, context_length), /v1/chat/completions
(auch stream und tools), Fehler als HTTP mit OpenAI-Fehlerobjekt. Hilfsseite
auf / zeigt Status, Base URL und Modelle. Fuer Programme in Docker: Netz
aria-llm, Name openai-rvs-wrapper. .env.example mit RVS_HOST/PORT/TOKEN.
(Ein OpenAI-Endpunkt direkt auf der VM war kurz drin und ist wieder raus —
der Wrapper braucht keinen offenen Port auf der VM.)
docs/LLM-SCHNITTSTELLE.md neu gegliedert: A) Cluster ueber RVS (empfohlen),
B) Wrapper, C) direkt an eine Box (Sonderfall), plus "Umstellen" fuer
bestehende Clients (Software-Schmiede).
Tests: mit echtem RVS und zwei echten llm-adaptern als zwei Boxen —
Modell einmal gelistet, 6 parallele Anfragen 3/3 verteilt, Box friert
mitten in der Anfrage ein → nach 15 s von der anderen beantwortet
(attempts=2), danach direkt die gesunde, nicht geladenes Modell →
model_unavailable ohne content. Wrapper als Docker-Container gegen das
offizielle OpenAI-SDK: models.list, create mit/ohne stream, NotFoundError,
AuthenticationError. Das Doku-Beispiel lief unveraendert (nur Adresse).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher konnte jeder Client per Modellname llama-swap zum Wechseln bringen —
ein Fremdprogramm haette ARIA das Modell unter den Fuessen weggetauscht.
Jetzt:
- Jede LLM-Karte einer Box (LLM_GPUS=1,2, alt LLM_GPU=1) haelt GENAU ein
Modell, dauerhaft. Welches, legt nur die Diagnostic fest. Karten, die nicht
fuers LLM vorgesehen sind, tauchen beim LLM nicht auf und werden als Ziel
abgelehnt.
- Clients (ARIA, Fremdprogramme ueber RVS, OpenHands ueber den OpenAI-Port)
sehen nur die geladenen Modelle und laden nie: Anfrage fuer ein anderes →
model_not_loaded mit Liste des Geladenen (RVS) bzw. HTTP 404 (OpenAI-Port).
Nichts wird verdraengt.
- Mehrere Clients koennen dasselbe Modell gleichzeitig nutzen.
llm-adapter: Belegung in /models/aria_slots.json. Die llama-swap-Config
enthaelt nur belegte Karten: pro Modell CUDA_VISIBLE_DEVICES=<Karte>, ttl 0,
groups.swap=false, hooks.on_startup.preload. Alles gegen llama-swap
ausprobiert (zwei Modelle gleichzeitig ready, keins verdraengt, Unbekanntes
404, env kommt beim jeweiligen llama-server an). llama-swap laeuft mit
--watch-config; die frueher aufgerufenen Reload-Endpunkte (/reload,
/api/config/reload) gibt es nicht (404). Neu: llm_unload_model (Karte leeren),
llm_models_request → llm_models (Erkundung, jede Box antwortet einzeln).
Fehlgeschlagenes Laden setzt die Karte auf ihr vorheriges Modell zurueck,
sonst risse jedes Neuladen die anderen Karten mit. Uebernahme: ohne
Belegungsdatei landet LLM_MODEL auf der ersten LLM-Karte.
Compose: llama-swap sieht alle Karten (NVIDIA_VISIBLE_DEVICES=all) und
zaehlt sie wie nvidia-smi (CUDA_DEVICE_ORDER=PCI_BUS_ID); das Pinnen pro
Modell macht der Adapter.
Bridge: uebernimmt models/slots auch aus worker_ping (nach "Entladen" sonst
30 s veraltet) und sagt sofort ab, wenn das Modell auf keiner Box liegt,
statt per Broadcast in den Timeout zu laufen.
Diagnostic: Flotte zeigt pro LLM-Karte Modell + Zustand + "Entladen";
Katalog waehlt eine Ziel-KARTE (leere vorausgewaehlt, Ersetzen mit
Rueckfrage); Download-Fenster nennt Karte und was ersetzt wird.
RVS: llm_unload_model, llm_models_request, llm_models freigegeben.
docs/LLM-SCHNITTSTELLE.md: Schnittstelle fuer Drittprogramme (RVS + OpenAI-
Port), Fehlercodes, Gleichzeitigkeit, Python-Beispiel.
Tests: 23 gegen llama-swap (Adapter direkt) + Ende-zu-Ende ueber den echten
RVS mit dem Adapter als Prozess: Laden per RVS mit Fortschritt, das
Doku-Beispiel unveraendert (nur Adresse) findet beide Karten und bekommt eine
Antwort, Absage/Schweigen wie dokumentiert, zwei Clients parallel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nach einem Sicherheitsfilter-Treffer fuhr ein Projekt still auf dem
Ausweich-Modell weiter, und nirgends war zu sehen, auf welchem. Zurueck ging
nur ueber "Liste leeren" im Filter-Panel, und das gleich fuer alle Chats.
Jetzt drei Ebenen (aria-brain/context_model.py):
Einstellung (brainModel) < pro Chat < Ausweich (Sicherheitsfilter)
- Pro Chat: Hauptchat und jedes Projekt koennen ein eigenes Modell haben, in
/shared/config/context_models.json. Bewusst kein Feld im Projekt-Manifest —
der Hauptchat ist kein Projekt und waere sonst aussen vor.
- "↩ zurueck" pro Chat: loescht nur die Filter-Vermerke DIESES Chats. Blockt
der Filter erneut, weicht das Brain eben wieder aus.
- Angezeigt wird, was wirklich laeuft: chat() gibt das Modell aus modelUsage
der CLI zurueck (aufgeloeste Version statt Alias) samt Grund; Brain → Bridge
→ chat_backup + Live-Payload → App und Diagnostic. Als Grundeinstellung
zaehlt das Modell, mit dem das Brain laeuft, nicht der Dateiwert — so faellt
ein ausstehender Neustart nach brainModel-Aenderung auf.
Diagnostic: 🧠-Auswahl neben "+ Projekt" fuer den fokussierten Chat, Modell im
Untertitel jeder Kontext-Karte, Modell statt "Claude" am Bubble-Badge (orange
mit ↪ bei Ausweich).
App: hinter der Quell-Anzeige (Einstellungen, Default aus — Kopfzeile bleibt
fuer Mama unveraendert) ein Modell-Chip in der Kopfzeile; Tippen oeffnet die
Auswahl fuer diesen Chat inkl. "Zurueck". Laeuft ueber den vorhandenen
brain_request-Kanal, also keine neuen RVS-Typen.
Tests: 47/47 (12 neue fuer die Aufloesungsreihenfolge gegen agent.chat()),
tsc ohne Fehler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beide hiessen bisher so, wie es in IHRER .env steht. Umbenennen hiess also: auf
den Rechner, Datei aendern, Dienst neu starten — beim Fire TV sogar neu
provisionieren. In der Oberflaeche gab es dafuer gar keinen Weg.
Jetzt ein ✎ pro Eintrag. Der Alias liegt zentral in
/shared/config/display_names.json ({"host": {...}, "satellite": {...}}) und
wirkt sofort fuer JEDEN schon ausgerollten Agenten, ohne ihn anzufassen.
Der entscheidende Teil ist die Rueckrichtung: die Bridge loest den Alias beim
ADRESSIEREN wieder auf (_resolve_host / _resolve_satellite). Ohne das haette man
etwas umbenennen koennen und es danach nicht mehr ansprechen — der Agent
vergleicht die Adresse gegen seine EIGENE Kennung und kennt den Alias nicht.
Angesprochen werden kann weiterhin alles: Alias, Eigenname oder technische ID.
Der Eigenname bleibt in der Liste klein daneben sichtbar, damit man den Bezug
zum Geraet nicht verliert. Leerer Name entfernt den Alias.
Diagnostic und Bridge teilen sich die Datei ueber /shared — kein neuer
Nachrichtentyp, keine Allow-List-Aenderung, eine Wahrheit.
Nebenbei behoben: _display_name_set legte fest "/shared/config" an, obwohl der
Dateipfad konfigurierbar ist. Damit scheiterte das Schreiben stillschweigend,
sobald der Pfad abwich. Verzeichnis wird jetzt aus dem Pfad abgeleitet.
17 Pruefungen: Aliasvergabe, Liste, Aufloesung in beide Richtungen,
Gross/Kleinschreibung, alter Name bleibt gueltig, anderer Agent unberuehrt,
beide Bereiche in einer Datei ohne sich zu stoeren, Entfernen, unbekanntes Ziel
wird nicht still umgebogen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ARIA sitzt woanders und kommt nicht ins Buero-Netz. Der Satellit kann beides:
er sieht das LAN und haelt eine ausgehende Verbindung zum RVS. Also reicht er
eine TCP-Verbindung durch — wie der VNC-Tunnel, nur fuer beliebige Ports.
Damit erreicht ARIA einen FreeCAD-MCP, eine Web-App, ein NAS-Interface.
Entscheidend fuer die Brauchbarkeit: die Bridge macht daraus einen GANZ
NORMALEN lokalen Port (aria-bridge:19000...). Dadurch funktioniert alles, was
sowieso TCP spricht — MCP-Client, curl, Browser — ohne Sonder-API. Jede
eingehende Verbindung bekommt einen eigenen Tunnel, parallele Verbindungen
sind also kein Problem (ein Browser oeffnet gern sechs).
Geraete-Namen: "duffy-desktop" ist ansprechbar, "192.168.1.47" nicht. Stefan
kann jedem gefundenen Geraet einen Namen geben, ARIA adressiert danach.
Schluessel ist die MAC, nicht die IP — per DHCP wandert die IP, die MAC bleibt.
Absicherung, weil das ein Weg in ein fremdes Netz ist:
- TUNNEL_ENABLED default AUS, pro Satellit.
- Ohne TUNNEL_ALLOW nur Ziele, die bei der Suche GEFUNDEN wurden. Der Tunnel
kann damit nichts ansteuern, was nicht ohnehin im LAN sichtbar ist — und
nichts im Internet.
- Obergrenze gleichzeitiger Tunnel + Reaper fuer ungenutzte (sonst bleibt eine
vergessene Verbindung offen, bis der Satellit neu startet).
- Faehigkeit 'tunnel' wird im sat_hello gemeldet, damit Diagnostic und ARIA
sehen, ob der Satellit das ueberhaupt erlaubt.
Neue RVS-Typen eingetragen (sat_tunnel_*, sat_name_*) — ohne das verwirft die
Allow-List sie still, und nichts funktioniert ohne Fehlermeldung.
16 Pruefungen gegen BEIDE echten Implementierungen (nur der RVS dazwischen ist
kurzgeschlossen): Namensaufloesung, Freigabe-Regeln, fremder Satellit
ignoriert, abgeschaltet blockt, echter Byte-Durchlauf (PONG:HALLO), unbekanntes
Ziel schliesst sauber statt zu haengen, Liste/Schliessen/Doppel-Schliessen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zwei Fehler, die zusammen wie "der Timer hat nicht gefeuert" aussahen.
1) Trigger hatten KEIN Projekt. Beim Feuern ging die Antwort ohne projectId
durch _process_core_response — also immer in den Hauptchat, auch wenn der
Timer mitten in einem Projekt-Thread gestellt wurde. Im Projekt blieb es
still, und es sah aus wie "nicht gefeuert". Jetzt wird das Projekt des
Requests mit angelegt, der getriggerte Turn laeuft darin, und die Antwort
kommt dort zurueck.
2) trigger_list zeigte nur "aktiv" / "DEAKTIVIERT" und verschwieg fire_count
und last_fired_at. Ein One-Shot-Timer wird NACH dem Feuern automatisch
deaktiviert — "hat sauber gefeuert" und "war nie an" sahen damit identisch
aus. ARIA hat genau das falsch gelesen und berichtet, sie habe vergessen den
Timer zu aktivieren, obwohl er puenktlich gefeuert hatte. Die Ausgabe nennt
jetzt ausdruecklich "SCHON GEFEUERT (Nx, zuletzt <Zeit>)" plus den Kontext,
in dem die Antwort landet.
Nachgewiesen am Fall flugwetter-whv-freitag-0900: gefeuert 07:00:05 UTC
(= 09:00 lokal), Antwort 46s spaeter, fire_count=1 — aber active=false im
Manifest und die Antwort im Hauptchat statt im Projekt-Thread.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher war die Eingabeleiste ein Entweder-oder: sobald Text im Feld stand oder
ein Anhang dranhing, verschwand das Mikro. Damit ging genau das nicht, was man
eigentlich will — ein Bild anhaengen und den Auftrag dazu SPRECHEN. Und ein
Zwischenruf ging nur getippt.
Das Mikro ist jetzt immer da; Senden-Pfeil und Mund-/Ohr-Knoepfe weichen je
nach Lage, damit die Zeile nicht ueberlaeuft.
Anhang + Sprachnachricht:
- Mikro antippen mit wartenden Anhaengen laedt sie SOFORT hoch (parallel zum
Reden) und markiert sie mit awaitVoice + clientMsgId.
- Die Bridge schickt solche Anhaenge nicht nach 0.8s alleine ans Brain, sondern
wartet auf den stt_endpoint und fuehrt Anhang + gesprochenen Satz zu EINEM
Turn zusammen. Ohne das sah ARIA ein Bild ohne Auftrag und danach einen Satz
ohne Bild.
- Der Merge ist strikt an die clientMsgId gebunden (require_match): eine
Sprachnachricht, die zufaellig waehrend eines fremden Uploads fertig wird,
reisst dessen Anhaenge nicht an sich. Der Text-Pfad behaelt seinen Fallback.
- Bild und gesprochener Text landen in EINER Bubble (gemeinsame audioRequestId).
- Klappt das Mikro nicht, bleibt die Bubble mit den Bildern stehen statt zu
verschwinden — die Dateien sind ja schon auf dem Server.
- Das Notfall-Fenster (420s) liegt ueber dem Aufnahme-Hard-Cap von 5 Minuten,
sonst waeren die Anhaenge bei einer langen Aufnahme vorher alleine raus.
Zwischenruf per Sprache:
- Der 📣-Knopf erscheint jetzt immer, wenn ARIA im Kontext arbeitet. Mit Text
schickt er sofort (wie bisher), ohne Text schaltet er die naechste Aufnahme
scharf (sichtbar hervorgehoben, gilt fuer genau eine Aufnahme).
- Eine so markierte Aufnahme geht per stt_stream_start als interject raus; die
Bridge schiebt sie in den laufenden Turn statt sie anzustellen.
- Dabei wird der laufende Turn NICHT abgebrochen (das war der Sinn) — nur die
Wiedergabe stoppt, damit ARIA nicht ins offene Mikro redet.
- Die Bubble zeigt 📣 Zwischenruf statt 🎤, damit man nicht auf eine eigene
Antwort wartet.
Merge-Logik gegen den echten Bridge-Code getestet (6 Faelle), tsc sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sat_hello und worker_hello kommen im Takt von ~25-30s erneut und wurden jedes
Mal mit "online" geloggt. Dadurch war das Bridge-Log praktisch unlesbar: die
interessanten Zeilen (Fehler, STT, Turns) gingen zwischen hunderten
identischen Meldungen unter.
Jetzt wird nur geloggt, wenn eine Box/ein Satellit NEU ist oder sich etwas
Echtes geaendert hat (Modell, GPUs, Standort, caps, control) — so wie es
host_hello schon vorher gemacht hat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vier unabhaengige Ursachen fuer 'erst beim zweiten Aufnehmen' bzw. 'wird gar
nicht verarbeitet' (server-/box-seitig, ohne App-Update wirksam):
1) rvs/server.js: stt_lease, stt_lease_request, stt_transcribe_blob,
stt_transcribe_result und chat_ack fehlten in ALLOWED_TYPES und wurden STILL
verworfen (dieselbe Falle wie bei worker_*/host_*). Folgen: das Lease lief
immer in den 250ms-Timeout -> jede Aufnahme startete verspaetet und OHNE
targetInstance nahmen alle STT-Worker dieselbe Session an (doppelte
stt_endpoint); chat_ack kam nie an -> getippte Nachrichten galten in der App
nach 3 Versuchen als fehlgeschlagen, obwohl sie verarbeitet waren.
2) xtts/whisper/bridge.py: Beim Kaltstart wurde die Session erst NACH dem
Modell-Load angelegt — alle Chunks der ersten Aufnahme liefen ins Leere und
der Take war weg (die zweite klappte, weil das Modell dann warm war). Session
wird jetzt sofort angelegt, der Endpointer wartet solange (runner.model None).
3) bridge/aria_bridge.py: Text-Dedup-Fenster 5s -> 1.5s. Es soll nur den
Zwilling zweier Worker abfangen; mit 5s fielen echte Wiederholungen weg —
also genau das Nachsprechen nach einem verschluckten Take, und kurze
Kommandos ('lauter', 'nächstes').
4) bridge/aria_bridge.py Voice-Router: Komma als Prefix-Trenner entfernt (nur
noch ':' und '-'), Fuzzy-Schwelle 0.6 -> 0.82, Sticky wird nicht mehr bei
jeder Nachricht verlaengert. Vorher matchten normale Saetze wie 'In Ordnung,
mach mal ...' als Projekt-Prefix: die Nachricht landete still in einem
Projekt und war im Hauptchat unsichtbar — verifiziert mit Beispielsaetzen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jede Host-Aktion wird als Historie-Eintrag festgehalten und in der Diagnostic
pro User-Nachricht gruppiert aufklappbar dargestellt (mit Screenshot-Thumbnails).
- Brain: trace_id+msg pro chat(); _dispatch_host als Wrapper um _dispatch_host_inner
emittiert je Aktion einen Eintrag an POST /internal/agent-history (Datei-Pfad aus
Screenshot-Text erkannt).
- Bridge: /internal/agent-history persistiert nach /shared/config/agent_history/
<hostId>.jsonl (Ringpuffer 200) + broadcastet agent_history; agent_history_query
-> agent_history_list.
- RVS: agent_history/_query/_list in ALLOWED_TYPES (sonst still verworfen).
- Diagnostic-Server: relay browser<->RVS + /uploads/<file> Bild-Endpoint (nur
/shared/uploads, kein Traversal).
- index.html: 'Historie'-Button je Host, Modal mit Akkordeon (Gruppe=Nachricht,
aufklappbar, Screenshot-Thumbs), Live-Refresh bei agent_history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Damit kann ARIA Host-Agenten tatsaechlich nutzen (Ende-zu-Ende):
- RVS: host_hello/host_ping/host_command/host_result in ALLOWED_TYPES.
- Bridge: _hosts-Registry (host_hello/host_ping/host_result), _host_list,
_host_request (requestId->Future wie Satelliten), HTTP-Endpoints
/internal/host-list und /internal/host (op via action).
- Brain: Tools host_list/host_exec/host_read/host_write/host_info/
host_screenshot + _dispatch_host (ruft /internal/host*). host_screenshot
speichert das PNG unter /shared/host-screenshots. host_exec kann sudo=true.
Naechste (optionale) Phase E: Host-Agenten in der Diagnostic-Flotte anzeigen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- diagnostic/server.js: die beiden Debug-Logs der ai-box-Fehlersuche entfernt
(roher RX-Dump jeder worker_*/sat_hello-Nachricht + 'worker_hello empfangen'
bei jedem 30s-Resend). Die Flotte laeuft, der Firehose kann weg.
- bridge/aria_bridge.py: /shared/logs/app.log (JSONL, wuchs unbegrenzt bei
jeder App-Log-Zeile) rotiert jetzt bei >5 MB auf die letzten 2000 Zeilen
(Muster wie metrics._maybe_rotate).
RVS selbst loggt nur Lifecycle (Connect/Disconnect) — kein Message-Firehose,
unveraendert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ARIA nutzt lokale LLMs auf mehreren ai-boxen; jede Box (llama-swap) kann
mehrere Modelle fahren. Auswahl nach MODELL, Box wird automatisch gewaehlt.
- xtts/llm-adapter/adapter.py: fragt beim Connect llama-swap GET /v1/models ab
und meldet die Modell-Liste in worker_hello (models:[...]), Fallback [LLM_MODEL].
- bridge/aria_bridge.py: Worker-Registry speichert models[]; _pick_worker(service,
model=) beruecksichtigt nur Boxen, die das Modell fahren koennen (nachsichtig:
keine → None → Broadcast/Claude-Fallback); Round-Robin je service+model
verteilt mehrere Projekte auf mehrere Boxen; _local_llm reicht das Modell durch;
_worker_list traegt models[].
- diagnostic/server.js: workers-Map + workerList um models[] erweitert.
- diagnostic/index.html: Compute-Flotte zeigt bei llm die Modell-Liste; das
"Lokales Modell"-Dropdown wird LIVE aus den angemeldeten Boxen gebaut
(Vereinigung + kuratierte Namen aus local_models.json, "· N Box(en)"/"offline"),
darunter eine kompakte LLM-Box-Liste (Node→Modelle→Health). Speisung aus dem
vorhandenen worker_update-Broadcast.
Kein Brain-/App-Eingriff. Routing greift nur bei aktivem Lokal-Schalter.
Deploy: diagnostic + bridge + llm-Boxen neu bauen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rein terminologisch: die feste "Gamebox" ist konzeptuell zu beliebig vielen
AI-Boxen geworden (Multi-Node-Compute-Fleet). Wort-Sweep ueber Code-Kommentare,
Strings, README, docs, .env.example, CHANGELOG — keine Identifier/Keys betroffen
(Gamebox kam nirgends in Variablennamen vor). Casing erhalten: Gamebox→AI-Box,
gamebox→ai-box. NODE_NAME-Default gamebox→ai-box, Instanz-IDs f5tts@ai-box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mehrere Instanzen pro Dienst nutzbar: Anfragen werden gezielt an eine freie
Instanz adressiert statt an alle gebroadcastet. Mehrere f5tts → naechstes
freies; mehrere LLM → parallele Turns (Multitasking); STT-Redundanz ueber
mehrere Apps via Lease.
- Worker (alle vier): filtern am Loop-Eingang — targetInstance gesetzt und
!= eigener INSTANCE_ID → Nachricht ignorieren. Feld fehlt → wie bisher.
- bridge (TTS/LLM, emittiert die Bridge selbst): _pick_worker() waehlt eine
online+freie Instanz (Round-Robin), stempelt targetInstance auf
xtts_request / llm_request. Keine Instanz bekannt → Broadcast.
- bridge (STT-Lease, emittiert die App): neuer stt_lease_request-Handler →
_pick_stt_worker() (voxtral vor whisper) → stt_lease {instanceId}.
- app (audio.ts): requestSttLease() vor dem Stream, stempelt targetInstance
auf stt_stream_start / stt_audio_chunk / stt_stream_end (+cancel). Kurzer
Timeout → '' (Broadcast), Aufnahme haengt nie.
Voll rueckwaertskompatibel: Routing aktiviert sich erst, wenn Worker sich per
worker_hello (Stage 2) registriert haben — sonst bleibt alles Broadcast.
Braucht APK-Rebuild fuer die STT-Lease-Seite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Wake-Word an" / "mach das Ohr an" / "hör wieder zu" / "wach auf" → App startet
den Listener wieder. Geht per Text-Nachricht ODER manuellem Aufnahme-Button —
beide laufen unabhängig vom Wake-Word-Listener, also funktioniert das Wieder-An
auch wenn das Ohr gerade taub ist (nur nicht per "Computer", das hört ja nicht).
Symmetrisch zu wake_off: Detektor _user_wants_wake_on → wake_on durch ChatOut →
Bridge → App löst wakeWordService.start() + setWakeWordActive(true) aus. An/Aus
kollidieren nicht (12 Fälle getestet). Damit: Ohr per Befehl ein UND aus, plus
weiterhin der Ohr-Button.
Fix nebenbei: gerades " in den Reply-Strings (hätte den Brain-Start gecrasht) →
einfache Anführungszeichen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Computer, Wake-Word aus" / "mach das Ohr aus" / "hör auf zuzuhören" / "geh
schlafen" → die App stoppt den Wake-Word-Listener KOMPLETT (Mikro frei, echte
Ruhe). Wieder-An nur über den Ohr-Button (bewusst, weil dann taub — Voice-
Wieder-An ist physikalisch unmöglich, wenn nichts mehr hört).
Deterministischer Detektor _user_wants_wake_off im Brain (kein LLM-Call nötig,
still). Signal fließt als wake_off durch ChatOut → Bridge → App-Payload; die App
löst denselben Pfad wie toggleWakeWord-off aus (cancelRecording +
wakeWordService.stop() + setWakeWordActive(false)). Detektor gegen 13 Positiv-
+ 9 Negativ-Fällen verifiziert (fängt nicht 'Licht aus' / 'Konversation Ende').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GPS: near()/entered_near()/left_near()-Watcher schalten das Tracking automatisch an bzw. beim Loeschen des letzten wieder aus; gpsTracking.start() sichert jetzt die Background-Permission. Cockpit-Fundament: neues Brain-Tool present_view emittiert aria_view (Orb + Karten), Bridge/RVS leiten weiter, ariaView.ts haelt die Spec App-seitig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ursache: Bridge-/chat-Aufruf hatte 1200s (20min) Timeout, der Proxy aber
24h. Lange Software-Dev-Turns dauern >20min → Bridge gab auf ('/chat
fehlgeschlagen: timed out'), der Brain lieferte die Antwort Minuten spaeter
(nur im Log, keine Bubble), und der Diagnostic-Kontext blieb auf 'running'
haengen (Folgenachrichten in die Queue trotz idler ARIA; nur Ctrl+R half,
verlor aber die Queue-Nachricht).
- Bridge: /chat-Timeout 1200s → 24h (env BRAIN_CHAT_TIMEOUT_SEC), passend
zum Proxy.
- Diagnostic: reconcileDiagStates() gleicht lokalen 'running'-Zustand gegen
die Brain-queue-status ab (2 Polls Karenz). Haengt ein Kontext, obwohl der
Brain nicht busy ist → Queue weiterschalten: angestellte Nachricht geht
automatisch raus statt verloren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neben Senden ein 'Zwischenruf' (App: oranger Button, nur wenn ARIA im
aktiven Kontext arbeitet; Diagnostic: Buttons neben beiden Senden). Geht
NICHT in die Queue und bricht NICHT ab — die Nachricht wird in den
laufenden claude-Subprozess geschoben; er greift sie an der naechsten
Tool-Grenze auf.
Technik:
- proxy-patches/manager.js: claude laeuft jetzt im --input-format
stream-json-Modus, initialer Prompt als stream-json User-Message,
stdin bleibt OFFEN; sendMessage() schiebt weitere User-Messages nach;
bei 'result' wird stdin geschlossen (Turn endet sauber). Ersetzt die
bisherigen sed-Patches (jetzt volle Datei via cp, docker-compose.yml).
- proxy-patches/routes.js: Side-Channel POST /interject {projectId,text}
→ subprocess.sendMessage der Kontext-Subprozesse.
- rvs: 'interject' erlaubt. bridge: RVS interject → Proxy /interject.
- App/Diagnostic: Zwischenruf-Buttons + lokale '📣 Zwischenruf'-Bubble.
Empirisch verifiziert: mid-turn injizierte Message wird an der naechsten
Tool-Grenze aufgegriffen (nicht mitten in einem blockierenden Befehl).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die App schickt Datei (fire-and-forget) und Text (ACK-getrackt, bei Queue
verzoegert) als getrennte RVS-Nachrichten; die Datei trug keine clientMsgId.
Die Bridge mergte gepufferte Files rein per Timing an den naechsten Text →
bei mehreren schnellen Nachrichten landeten Bilder beim falschen Text.
App: file-Send traegt jetzt dieselbe clientMsgId wie der Text.
Bridge: puffert Files mit cmid und merged beim Text-Flush NUR die Files mit
passender cmid; Rest bleibt fuer seine eigene Nachricht gepuffert. Fallback
(Legacy/kein Treffer) = altes Verhalten, damit nie ein Bild verloren geht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kern des "Verbinde..."-Hangs: host.docker.internal loeste zu 172.17.0.1 (docker0)
auf, QEMUs VNC ist aber ans aria-net-Gateway 192.168.64.1 gebunden (dorthin
bindet der Brain) → Connection refused. Die Bridge ermittelt den VNC-Host jetzt
selbst per _docker_gateway() (/proc/net/route) = dasselbe Gateway wie der Brain.
ARIA_VNC_HOST-Env hat weiter Vorrang.
Live belegt: Bridge->192.168.64.1:5901 liefert RFB-Handshake, ->host.docker.
internal:5901 (172.17.0.1) = refused.
py_compile clean. Deploy: bridge rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Voice-Projektwechsel ist zurueck, aber nur fuer das ausloesende Geraet:
- App merkt sich die IDs eigener Anfragen (Text-clientMsgId via dispatchWithAck +
Voice-audioRequestId an allen 4 Aufnahme-Stellen) in myRequestIdsRef.
- Bridge haengt an project_changed die ausloesende clientMsgId an — in beiden
Voice-Pfaden: send_to_core (ARIAs project_enter/exit) und _process_endpoint_text
(Voice-Router back_to_main / project_prefix).
- App folgt einem project_changed-Wechsel nur, wenn die clientMsgId eine eigene
ist → andere App-Instanzen + Diagnostic bleiben unberuehrt.
Nur Bridge + App betroffen. py-compile + tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QEMU-VNC erscheint live in der App-Desktop-Kachel, komplett ueber RVS:
- Bridge: TCP↔RVS-VNC-Bruecke. vnc_open oeffnet asyncio-Verbindung zu
host.docker.internal:5901, Reader-Loop streamt RFB-Bytes als vnc_data;
vnc_input schreibt zurueck; vnc_close raeumt auf. check_desktop probt den
Port und meldet desktop_status. Base64-in-JSON, kein websockify/noVNC auf
dem Host noetig.
- App: novncHtml.ts laedt noVNC (CDN) und ersetzt window.WebSocket durch einen
Shim, der RFB-Bytes per postMessage ueber RVS brueckt (server-speaks-first →
timing-robust). VncTile mountet die WebView nur im Fokus, oeffnet bei 'ready'
den Tunnel (desktop.openVnc), speist Server-Bytes ein und schickt Eingaben
als vnc_input; beim Verlassen wird der Tunnel geschlossen (VM laeuft weiter).
tsc/py clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Durchgaengiger Live-Editor fuer Code-Projekte:
- App: selbstenthaltener Highlight-Editor in einer WebView (editorHtml.ts,
Textarea + Regex-Highlight-Layer, voll offline). CodeEditorTile mit Datei-Tabs,
verdrahtet mit dem codeFile-Spiegel; Bridge-Protokoll setContent/applyPatch/
setReadOnly ↔ onEditFromUser.
- Proxy-Hook (routes.js): faengt ARIAs Write/Edit/MultiEdit unter
/shared/projects/<pid>/ ab und postet den Volltext bei Erfolg an
/internal/code-file. tool_use_id→file_path-Korrelation, liest die Datei aus
dem gemounteten /shared.
- Bridge: /internal/code-file relayt als RVS code_file an die App; eingehende
code_file_edit schreiben den Volltext pfad-sicher nach /shared/projects/<pid>/
(_write_project_file, kein Ausbruch via ..).
Arbeitsverzeichnis fuer Code-Projekte = /shared/projects/<projectId>/ (in proxy/
bridge/brain gemountet, kein SSH noetig). tsc/py/js clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zweite Nachricht waehrend ARIA arbeitet wird jetzt ANGESTELLT statt den
laufenden Task abzubrechen. Stellt ARIA eine blockierende Rueckfrage, pausiert
die Queue und die naechste Eingabe beantwortet sie — bis eine finale Antwort
kommt, dann laeuft der naechste Queue-Eintrag (pro Projekt unabhaengig).
- Brain: ARIA deklariert Rueckfragen per unsichtbarem [[AWAIT]]-Marker
(wie speak/converse; kein '?'-Raten). _extract_await_marker strippt ihn,
chat() gibt 5-Tupel (+awaiting_reply), System-Prompt erklaert den Marker.
- Bridge: awaiting_reply aus Brain-Response in die chat-Broadcast-Payload.
- App: app-lokale Queue + Zustandsautomat (idle/running/awaiting_reply) pro
Projekt; Send-Flow von Abbruch auf Anstellen; Stop-Button (cancelRequest)
schaltet die Queue weiter; sichtbare pending_queue-Bubbles (tippen loescht);
Rueckfrage-/Queue-Banner ueber dem Eingabefeld. Voice bricht nicht mehr ab
(haltet nur TTS, serialisiert im Brain-Lock); Text-Send erkennt Brain-busy
als Fallback. Pro-Projekt-Textfeld-Entwuerfe (Draft-Map + AsyncStorage).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die Live-chat-Payload trug keine files — Anhaenge kamen nur als separates
file_from_aria-Event (eigene, teils unsichtbare Bubble). An der Text-Nachricht
tauchte die Datei erst nach einem Seitenwechsel auf (Reload aus chat_backup,
das die files kennt).
- Bridge: files jetzt direkt in der chat-Payload (selbe Struktur wie Backup).
- App: chat-Handler haengt payload.files an die Text-Bubble (wie der Reload-Pfad)
und entfernt die redundante Solo-file_from_aria-Bubble mit passendem serverPath.
- App: file_from_aria legt keine Doppel-Bubble an, wenn die Datei schon an einer
Nachricht haengt (Event-Reihenfolge-unabhaengig).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude haengt manchmal reflexartig ein leeres <voice></voice> an (v.a. bei
Spotify-Antworten). clean_text_for_tts nahm dann group(1)='' → text='' → gar
keine Sprachausgabe. Das erklaerte, warum Playlist-Wechsel 'Fliegen' vorgelesen
wurde (Claude schrieb Inhalt ins Tag), 'Prodigy' aber stumm blieb (leeres Tag)
— reiner Claude-Output-Zufall, nicht Skill/Playlist-Name.
Fix: leeres/whitespace <voice> ignorieren und den normalen Anzeigetext lesen.
Deterministisch, unabhaengig von Claudes Tag-Reflex.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verallgemeinert die Ausgabesteuerung, dynamisch statt hardcoded:
- Zwei Flags: speak (vorlesen?) + converse (danach 30s weiterlauschen?).
Getrennt, weil 'vorlesen' und 'Dialog offen halten' verschiedene Dinge sind
('was laeuft gerade' → vorlesen JA, aber keine 30s).
- Statischer Manifest-Default (speak/converse) PLUS: der Skill kann beide im
JSON-Output PRO AUFRUF setzen und den Default ueberschreiben — so kann EIN
Skill gemischt sein (Spotify: 'next' stumm/stop, 'was laeuft' vorlesen/stop).
- chat() gibt jetzt (reply, answered_by, speak, converse) zurueck; gilt fuer
Fast-Path (converse immer False), local UND Claude (run_*-Skill setzt beide).
- Brain: _last_skill_flags aus Skill-stdout-JSON, _skill_response_flags mergt
Output > Manifest > False. main.py ChatOut.converse, background.py angepasst.
- Bridge: converse aus /chat gelesen + in Chat-Payload + _process_core_response.
- App: converseRef aus der Payload; onPlaybackFinished endet mit skipPassive
wenn converse=false (vorlesen ohne 30s).
- Skill-Bau-Anleitung (skill_create/update-Schema): speak+converse dokumentiert
MIT dem WARUM, damit ARIA sie beim Bauen sinnvoll setzt (nicht nur mechanisch).
- Prompt-Hardcode fuer Spotify-Faehigkeiten raus → Skills beschreiben sich selbst.
Bestehende Skills ohne Flags = false/false = stumm/stop (kein Verhaltenswechsel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Reverse-Geocode lief nur async im Hintergrund — die ERSTE Nachricht an
einem Ort (v.a. direkt nach Bridge-Neustart, Cache leer) wurde gebaut BEVOR
der Name fertig war → Präfix ohne Ort → local suchte Wetter mit rohen
Koordinaten und fand nichts.
Neu: _ensure_place_name() awaitet den (laufenden oder frisch gestarteten)
Geocode kurz mit Timeout, bevor der Standort-Präfix gebaut wird. Gecacht →
nur die erste Nachricht an einem neuen Ort zahlt ~0,5s, danach instant.
Timeout/Fehler → Fallback auf reine Koordinaten (kein Hänger).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neben "nordwestwaerts" jetzt auch die genaue Bearing-Gradzahl (0-359) —
"unterwegs nordwestwaerts (304 Grad)". Die KI nutzt, was die Frage braucht
(Pannenhilfe → Ort/km, Luftfahrt/Navigation → exakte Peilung).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aus dem permanenten GPS wird die Bewegungsrichtung berechnet: Bearing zwischen
einem Anker-Punkt und der aktuellen Position, gemappt auf 8-Wind-Adverb
(nordwestwaerts …). Der Anker rueckt erst bei echter Bewegung (>25 m) nach,
sonst zaehlt GPS-Jitter im Stand nicht — Stehenbleiben behaelt die letzte
Richtung, Umdrehen liefert automatisch die neue.
Praefix jetzt z.B.:
[Stefans aktueller Standort: A 28 bei km 55,6, Oldenburg, Niedersachsen
(GPS 53.12, 8.22), unterwegs nordwestwaerts. ...]
Reiner Bridge-Fix (Haversine + Bearing lokal, kein Dienst noetig).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Erweitert den Reverse-Geocode: Nominatim zoom=18 → Straße + Hausnummer, PLZ,
Stadt, Bundesland ("Am Wunderburgpark 6, 26135 Oldenburg, Niedersachsen").
Bei Autobahn/Bundes-/Kreisstraße wird die Ref (A 28 / B 75 / K …, aus
extratags) vorangestellt + best-effort Kilometer via Overpass-Milestone
(highway=milestone, distance-Tag, naechster im Umkreis) → z.B.
"A 28 bei km 55,6, Oldenburg, Niedersachsen". Für Pannenhilfe & Co.
Cache-Raster von ~1km auf ~110m verfeinert (Straßen-Genauigkeit). Overpass
nur wenn's nach Straße/Autobahn aussieht (ref/Autobahn im Namen). Alles
best-effort mit stiller Fallback-Kette (Fehler → gröberer Ort → nur GPS).
NOMINATIM_URL + OVERPASS_URL env → self-hostbar.
Richtung ("Richtung Leer") bewusst weggelassen — nicht zuverlaessig aus
einem Punkt ableitbar (braucht Fahrtrichtung/Routing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Das lokale 8B kann Koordinaten nicht zuverlässig in eine Stadt uebersetzen
(riet "Berlin" fuer Oldenburg). Claude kann's, aber das kostet Tokens.
Loesung: die Bridge loest lat/lon EINMAL via Nominatim (OpenStreetMap, gratis,
keyless) auf und schreibt den Namen in den Praefix:
[Stefans aktueller Standort: Oldenburg, Niedersachsen (GPS 53.12, 8.22). ...]
So muss KEIN Modell mehr raten — local UND Claude lesen den Ort direkt ab,
Token-Ersparnis bleibt voll erhalten.
- _persist_location merkt sich den letzten Ort + triggert den Geocode async
(im Hintergrund, gecacht pro ~1km-Raster, nur bei Bewegung → Nominatim-
Rate-Limit locker eingehalten).
- _build_core_text nutzt frische ODER letzte bekannte Position (Praefix faellt
beim passiven Weiterreden ohne frisches GPS nicht mehr weg) + den Ortsnamen.
- NOMINATIM_URL env (Default oeffentlicher Dienst) → spaeter self-hostbar.
Reiner Bridge-Fix, kein APK/Brain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fast-Path-Antworten sind speak=False → kein TTS → onPlaybackFinished feuert
nie. Das Wake-Word-Re-Arm haengt aber genau daran → nach "nächster Titel"
blieb das Ohr grau in 'conversing' stecken (Stefan im Auto, 2 Min gewartet,
kein Re-Arm). Log bestaetigt: nach stream.final kam kein wake.end/wake.start.
- Bridge: chat-Payload traegt jetzt 'speak' (war nur answeredBy).
- App: bei stiller Antwort (speak=false ODER answeredBy=fast-path) und
laufender Konversation dieselbe Re-Arm-Logik wie bei TTS-Ende anstossen
(aktiv → resume/Konversationsfenster, sonst → endConversation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Im Auto cancelt der App-seitige No-Speech-Watchdog den conversing-Stream
vorzeitig (Whisper-Partials kommen bei Fahrgeraeusch verspaetet) und startet
passives Lauschen, waehrend die Bridge dieselbe Aeusserung parallel noch
finalisiert. Ergebnis: derselbe Befehl ("nächstes lied") wird zweimal
ausgefuehrt — aus zwei verschiedenen audioRequestIds, darum griff die
bestehende ID-Idempotenz nicht.
Text-Fenster-Dedup: identischer normalisierter Endpoint-Text innerhalb 5s
wird verworfen. Serverseitig, kein APK noetig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Ursachen, beide adressiert:
- App raeumte den (kontext-scoped) Thinking-Indikator nur ueber das
agent_activity 'idle' der Bridge. Kam das nicht an (dedup/mismatch),
blieb "ARIA denkt" + Abbrechen stehen. Jetzt raeumt die App den
Indikator beim Eintreffen der Antwort selbst (definitiver Turn-Ende-Beweis).
- Bridge sendete 'thinking' mit der REQUEST-projectId, 'idle' aber mit der
TURN-projectId (Brain kann umrouten, z.B. Voice-Sticky). Bei Abweichung
bekam der Request-Kontext nie sein idle → zusaetzliches idle fuer die
Request-projectId am Turn-Ende.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regression seit das lokale LLM leichte Turns (Wetter, Spotify) beantwortet:
es nutzt bewusst KEINE <voice>-Tags, an denen frueher die Zahl-Aussprache hing.
clean_text_for_tts schrieb nur Uhrzeiten/Dezimalzahlen/Zahl+Einheit aus, nie
freistehende Ganzzahlen ('23°C', '100%', '7 Nachrichten').
Neuer vollstaendiger Konverter _int_to_words_de (0..999999) + generischer
Durchlauf am Ende von clean_text_for_tts, der alle uebrigen Ganzzahlen zu
Woertern macht. Tag-unabhaengig -> gilt fuer local UND claude. Lange
Ziffernfolgen (IDs/Codes, >6 Stellen) bleiben Ziffern; an .,:/ klebende
Zahlen (IPs, Versionen) werden uebersprungen. Nebenbei: fehlendes Leerzeichen
bei '23°C' -> 'dreiundzwanzig Grad Celsius' gefixt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stefans Idee: die QUELLE entscheidet ueber Vorlesen, nicht der Reply-Inhalt.
Fast-Path (reiner Steuerbefehl) = speak=False (stumm); ARIA-Antworten
(local/claude) = speak=True (vorlesen ok — eine Ansage ist sogar nett).
- agent.chat() gibt jetzt (reply, answered_by, speak) zurueck; main.py
ChatOut.speak; background.py-Aufrufer angepasst.
- bridge: liest speak aus /chat, reicht es an _process_core_response; bei
speak=False wird TTS uebersprungen — VOR jeder <voice>-Logik.
Robust: unabhaengig davon, ob die Skill-fast_pattern-Reply ein <voice></voice>
enthaelt (das verlor ARIA beim semantischen Skill-Rebuild — genau der Bug).
App braucht keinen Change: kein xtts_request -> kein Audio -> stumm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mehrere lokale Modelle, on-demand geladen/geswappt, in Diagnostic waehlbar.
Design: das Brain schickt den Modellnamen (aus local_llm.json) im llm_request
mit -> Adapter -> llama-swap laedt/swappt. Keine separate Gamebox-Config noetig.
- xtts: `llama`-Container -> `llama-swap` (unified-cuda), config.yaml mit
qwen3-8b (Standard) + qwen3-4b; Auto-Download via -hf, Cache /models geteilt
(qwen3-8b schon da). Adapter -> llama-swap:8080, Timeout 600s (Erst-Download).
- adapter: `model` aus dem Request an llama-swap durchreichen (Fallback env).
- brain: router.load_config liest localLlmModel; local_llm_chat(model=...);
agent gibt cfg-Modell mit; bridge reicht model durch (_local_llm + Route).
- diagnostic: /api/local-models-list (aus /shared/config/local_models.json,
seeded), local-llm-config um localLlmModel erweitert; Dropdown "Lokales
Modell" im Settings-Block + Erst-Download-Hinweis.
BLIND gebaut (Gamebox nicht testbar hier): llama-swap CLI/Config-Pfad beim
ersten Start via `docker logs aria-llama-swap` pruefen. Live-Lade-Status
(Adapter->Diagnostic) ist B0.5-2 (Folgeschritt).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zeigt pro Antwort, welcher Backend sie erzeugt hat — farbcodiert (lokal=gruen,
Claude=blau, Fast-Path=lila). Nur in Diagnostic (App bleibt Mama-tauglich).
- agent.chat() gibt jetzt (reply, answered_by) zurueck; an jedem Return-Punkt
gesetzt (fast-path/local/claude). Beide Aufrufer (main.py, background.py)
angepasst. main.py: ChatOut.answered_by.
- bridge: liest answered_by aus /chat, reicht es an _process_core_response,
broadcastet es im chat-Payload UND persistiert es in chat_backup (answeredBy)
→ bleibt nach Reload.
- diagnostic: srcBadgeHtml() rendert den Badge in Live-Chat + History;
server.js liefert answeredBy in der chat_history.
Erweiterbar: spaeter kann ein Bild-Backend (FLUX/Modellname) denselben Kanal
nutzen. Modellwechsel-fuer-Antworten (groessere Modelle bei mehr GPUs) bleibt
B0.5/Skalierungs-Thema im Plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brain → HTTP /internal/local-llm → Bridge → RVS → llm-adapter → llama.cpp,
1:1 nach dem FLUX-Roundtrip-Muster gebaut:
- Bridge: _pending_llm (requestId→Future), llm_response-Handler (setzt Future),
_local_llm() (sendet llm_request, wartet mit 30s-Timeout), HTTP-Route
POST /internal/local-llm ({messages, max_tokens?, temperature?, stop?}).
- Brain: local_llm.py mit local_llm_chat() — POSTet an die Bridge, gibt
{ok, content, model?, elapsedMs?} zurueck, wirft nie (Aufrufer eskaliert
bei ok=false auf Claude).
Provider-Kette bereits verifiziert (718ms). Als naechstes: Test brain→bridge→
gamebox end-to-end, dann B1 (Router-Heuristik + Escalation) und der
Diagnostic-Testchat/Status (B0.5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher: tts_text = tts_text_preview or text — ein leeres <voice></voice> fiel
auf den vollen Text zurueck und wurde doch gesprochen. Jetzt: ein vorhandener
<voice>-Tag ist die EXPLIZITE TTS-Vorgabe (auch leer). Leeres <voice></voice>
= Bubble sichtbar, aber KEIN TTS. Nur ohne Tag Rueckfall auf vollen Text.
Motivation: Spotify-Steuerbefehle (next/pause/…) laufen jetzt ueber den
Fast-Path (<1s), aber die gesprochene Quittung klaute auf dem Handy den
Audio-Fokus -> Spotify duckte/pausierte und spielte (bei kurzem Text) nicht
weiter. Steuer-Skills koennen ihre Reply nun mit <voice></voice> stummschalten.
Generell nutzbar: ARIA kann jede Antwort bewusst stumm halten.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>