Punkt 2: das Wake-Wort triggerte nur im Vordergrund — eine bewusste JS-Zeile
verwarf jede Hintergrund-Erkennung (native Erkennung + Foreground-Service
liefen längst durch). Jetzt hinter Einstellung 'Auch bei gesperrtem Bildschirm
zuhören' (default aus, mehr Fehltrigger möglich). Greift live via
setBgWakeEnabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nachdem die Stille-Toleranz jetzt überall der einzige Wert ist, sind die alten
Settings obsolet — raus damit:
- audio.ts: CONV_WINDOW_* Konstanten + loadConvWindowMs() entfernt.
- wakeword.ts: PASSIVE_LISTEN_* + load/savePassiveListenMs() entfernt; der Passiv-
Master-Timer nutzt jetzt einen festen internen Backstop (PASSIVE_BACKSTOP_MS,
15s) statt eines Nutzer-Werts — das echte Ende regelt die Stille-Toleranz.
- SettingsScreen: "Konversations-Fenster"- und "Weiterreden-Fenster"-Slider raus;
Letzterer durch eine kurze Erklärung ersetzt (verweist auf Stille-Toleranz).
- ChatScreen: tote Imports weg.
Kein Verhaltensänderung ggü. ebe0e80 — nur Aufräumen. tsc grün.
Deploy: neue APK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stefans Modell: es braucht keinen separaten 30s-Wert. Nach ARIAs Antwort geht das
Mikro auf; fängst du nicht innerhalb der Stille-Toleranz (z.B. 5s) an zu reden, ist
Schluss → zurück aufs Wake-Word. Derselbe Wert wie die Pause-Toleranz beim Reden.
Ein Befehl ohne "fortführen" = Ende; nach einer normalen Antwort = ein Stille-
Fenster zum Weiterreden.
- Alle Aufnahme-Pfade (wake/barge/passiv): noSpeechTimeoutMs = endpointMs =
loadSttEndpointMs() statt loadConvWindowMs()/loadPassiveListenMs(). Ein Wert.
- Passiv-Hardcap: loadMaxRecordingMs() statt fix 35s (schnitt langes Reden ab).
- Leeres Endpoint im listening-State: KEIN Re-Arm mehr → exitPassiveListening
(ein 5s-Fenster, dann Ende). Der 30s-Master-Timer in wakeword.ts wird dadurch
nie mehr scharf (harmloser Backstop).
Redest du weiter → Antwort → wieder ein Stille-Fenster (Multi-Turn bleibt, nur ohne
30s-Leerlauf). Die Settings "Konversations-Fenster"/"Passiv-Lauschen" sind damit
obsolet (UI-Cleanup später).
Deploy: neue APK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Voice-First: ARIA deklariert die Phase aus dem Text per Inline-Marker — dasselbe
Muster wie [[AWAIT]], die Marker werden vor Anzeige/TTS/History entfernt. Loest:
- Claude-Pfad las Steuerbefehle vor, obwohl das Skill lief (Manifest-speak-Flag
kennt den Unterschied Befehl/Frage nicht — dasselbe Skill ist mal Aktion, mal
Auskunft). Jetzt entscheidet ARIA aus dem Kontext.
- Befehlsketten (VNC oeffnen, Menue klicken, ...): Mikro soll offen bleiben und
stumm gearbeitet werden, bis Stefan die Kette beendet.
Marker (ARIA haengt sie ans Antwort-Ende):
[[STUMM]] -> Steuerbefehl, nicht vorlesen. Allein = Einzelbefehl -> danach Stop.
[[WEITER]] -> Konversation/Kette laeuft weiter -> Mikro offen halten.
[[ENDE]] -> Konversation/Kette beenden -> zurueck aufs Wake-Word.
Brain:
- _extract_flow_markers() zieht [[STUMM]]/[[WEITER]]/[[ENDE]] aus dem Claude-Reply
und ueberschreibt speak/converse (Marker ist autoritativ ueber Skill-Flag).
[[STUMM]] allein impliziert converse=false; [[ENDE]] schlaegt [[WEITER]].
- prompts.py: neue Sektion build_voice_flow_section() bringt ARIA Marker +
Befehl/Frage-Klassifikation + Ketten-/Ende-Erkennung bei.
App (ChatScreen): stiller Antwort-Zweig verzweigt jetzt auf converse —
converse=true (Kette) -> endConversation(false), Mikro bleibt offen fuer den
naechsten Kettenbefehl; converse=false -> ariaStopRecording (Stop wie gehabt).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Steuerbefehle laufen stumm (speak=false in Fast-Path/Local/Claude-Pfad) — ohne
TTS feuert aber onPlaybackFinished nie, und daran hing bisher das Aufraeumen der
Aufnahme. Folge: nach "spotify play" blieb das Mikro-/Konversations-Fenster offen
(30s Leerlauf), obwohl der Befehl laengst ausgefuehrt war.
Neu: ariaStopRecording() — das programmatische Gegenstueck zum Stop-Button. Nach
einer stillen Antwort (speak=false) schliesst ARIA jede offene Aufnahme selbst und
geht zurueck aufs Wake-Word, egal in welchem Zustand: passives Lauschen sauber
beenden, offene Streaming-Aufnahme (aktiv/Barge) verwerfen, sonst endConversation.
Vorher nur der conversing-Fall abgedeckt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stop finalisierte die Aufnahme, aber die danach kommende onPlaybackFinished oeffnete via converseRef erneut das 30s-Passiv-Fenster. Jetzt setzt handleVoiceButtonStop converse=false → nach manuellem Stop kein Passiv-Lauschen mehr.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
converse defaultete true → jeder Befehl mit gesprochener Bestaetigung ('Spiele Spotify') ging ins 30s-Passiv-Fenster. Jetzt nur bei explizitem converse:true vom Brain; einzelne Befehle enden sofort → zurueck aufs Wake-Word, Spotify resumed gleich.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Behebt den Konversations-Mischmasch: (1) Barge-in-Einstellung (Default AUS = Halb-Duplex) gated das Mikro-Lauschen waehrend TTS -> ARIA spricht ungestoert zu Ende. (2) interruptAriaIfBusy bricht die Brain-Antwort WIRKLICH ab (cancel_request) statt nur TTS zu muten -> sie antwortet nicht mehr weiter waehrend man redet. (3) Stop-Button stoppt vorhersehbar alles (TTS + Brain-Cancel). Musik: nativer Focus ist schon GAIN_TRANSIENT (pausiert) -> der saubere Flow behebt das Focus-Geflacker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der sichtbare 'Stille-Toleranz'-Regler verstellte den toten Legacy-dB-VAD-Pfad; jetzt steuert er STT_ENDPOINT_MS (die echte Streaming-Pausen-Toleranz, 1-4s). Der obsolete 'Stille-Pegel (dB)'-Regler ist entfernt — der aktive STT nutzt adaptiven Rausch-Boden, Rauschen-als-Wort verhindert der no_speech_prob-Filter des Modells.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wake-Word/Barge-In waren hart auf 60s gecappt (schnitt lange Diktate bei 1 min ab). Jetzt lesen sie loadMaxRecordingMs() — der bestehende, aber vom Streaming-Pfad abgeklemmte 'Maximale Aufnahmedauer'-Regler (1-30 min) steuert nun wirklich. Voice-Bubbles zeigen die Aufnahmedauer (durationS aus stt_endpoint) als 'M:SS'.
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>
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>
Zwei Kopplungs-Bugs behoben:
- set_project_kind nutzte den globalen active_project statt des Request-Projekts
→ markierte das falsche Projekt (belegt: global aktiv war mac_os_update_fehler,
das kind=code bekam, obwohl die App in basic_os war). _dispatch_tool bekommt
jetzt die project_id des Requests durchgereicht; set_project_kind wirkt darauf.
- Die App erzwang bei project_changed-Broadcasts (ARIA-Tool/Diagnostic/andere
App-Instanz) einen Fokuswechsel → alle Geraete wurden mitgezogen. Fokus ist
jetzt rein geraetelokal; Broadcasts aktualisieren nur Namen/Typ. Wechseln nur
noch lokal ueber den Drawer.
py-compile + tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Progressive Reveal: ChatScreen spiegelt bei project_changed den Projekt-Typ
sofort in projectFocus (kind_changed nach set_project_kind) → Editor/Desktop-
Kacheln erscheinen live, nicht erst beim Reconnect.
- useWorkspaceLayout: merkt pro Projekt die zuletzt fokussierte Kachel
(AsyncStorage aria_workspace_layout) und stellt sie beim Zurueckkehren in ein
Code-Projekt wieder her.
tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verdrahtet die neuen Kanaele "dunkel" (noch ohne UI):
- rvs/server.js: ALLOWED_TYPES um code_file/code_file_edit, check_desktop/
desktop_status und vnc_open/close/data/input erweitert (Base64-in-JSON-Relay
wie audio_pcm, kein Binaer-Handling noetig).
- brainApi.ts: Project.kind ('code'|'chat') + desktop_url; ChatScreen publiziert
die Kinds in den projectFocus-Spiegel.
- services/codeFile.ts: Live-Spiegel der Code-Dateien (content + Deltas) mit
Ruckkanal code_file_edit fuer eigene Edits.
- services/desktop.ts: Desktop-Status + VNC-RFB-Tunnel (vnc_open/close/input/
data) durch RVS, Session = Projekt-ID.
tsc clean, rvs/server.js syntaktisch OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neues Singleton services/projectFocus.ts (Publish/Subscribe wie rvs.ts): haelt
focusedProjectId, Projekt-Namen und Projekt-Kind. ChatScreen publiziert Focus +
Namen EINWEG hinein (rein additive Effekte), damit der kommende Workspace-Canvas
den aktiven Kontext und den Code-Projekt-Status kennt, ohne dass ChatScreen den
Workspace kennt oder umgebaut wird. Verhaltensneutral, tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei Ursachen:
- pending_queue-Bubbles haben (noch) keine clientMsgId → der Reconnect-
History-Sync erkannte sie nicht als lokal-only und verwarf sie, waehrend
projectQueues den Eintrag behielt → 'N in Warteschlange' fror ein ohne
sichtbare Nachricht. Jetzt bleiben pending_queue-Bubbles beim Sync erhalten.
- Watchdog: haengt ein Kontext >15s auf 'running', obwohl der Brain ihn NICHT
als busy meldet (Antwort beim Abbruch verloren), schaltet die Queue jetzt
selbst weiter (dequeue oder idle) statt fuer immer zu blockieren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher war die serialisierte Sprachausgabe zweier fast gleichzeitig fertiger
Antworten Timing-Glueck: PcmStreamPlayer.start() ruft stopInternal() (flush+
release), eine neue Antwort haette die laufende also abgeschnitten, sobald ihr
Audio waehrend der Wiedergabe der ersten ankam.
Jetzt echte Abspiel-Queue im audioService: kommt eine neue HOERBARE Antwort
waehrend eine andere noch hoerbar spielt (pcmAudiblePlaying bis
PcmPlaybackFinished, nicht nur bis Stream-Ende), werden ihre PCM-Chunks
gepuffert und erst nach dem Drain der laufenden nachgespielt. Bei wartender
Antwort meldet PcmPlaybackFinished NICHT 'fertig' (kein Wake-Word-Re-Arm).
Harter Stop/Barge-In/Mute verwirft die Queue. Race gegen gleichzeitige Chunks
einer dritten Antwort geschlossen (Flags vor await gesetzt). onPcmCached meldet
den WAV-Pfad nachgespielter Antworten fuer Mund-Button-Replay.
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>
sendTextMessage stand vor interruptAriaIfBusy und sendPendingAttachments,
hatte beide aber im deps-Array → Temporal Dead Zone (TS2448/2454). Lief nur,
weil Babel const→var hebt (deps auf erstem Render undefined, Body laeuft erst
bei Interaktion). Deklaration hinter beide verschoben — Abhaengigkeit ist
einseitig, sendTextMessage wird nur in der JSX genutzt. tsc-clean.
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>
Ohne Ohr (manuelle Aufnahme) + Stop-Button: das echte STT-Endpoint (mit Text)
fuellt die Bubble, aber danach klappt ein zweites, LEERES stream_end-Endpoint
nach (Audio schon transkribiert). Der Empty-Handler loeschte die Bubble
bedingungslos per audioRequestId — auch die schon mit Text gefuellte.
Ergebnis: Bubble weg, obwohl ARIA an der Antwort arbeitet; erst nach
App-Neustart (aus chat_backup) wieder da.
Fix: leeres Endpoint entfernt nur noch den NOCH UNAUFGELOESTEN Platzhalter
(Text enthaelt 'Spracheingabe wird verarbeitet'), nicht eine bereits
aufgeloeste Bubble.
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>
- router.py: die run_spotify-Faehigkeiten NICHT mehr im Prompt aufzaehlen
(war selbst Hardcoding). Generisch: "run_*-Skills — was sie koennen steht in
IHRER Tool-Beschreibung, lies + nutze sie fuer alles Passende". Skills
beschreiben sich selbst (dynamisch, ARIA-authored). Anti-Halluzination bleibt.
- App: Stop-Button waehrend passivem 30s-Lauschen ('listening') beendet jetzt
sauber (exitPassiveListening) statt via leerem Endpoint den Passiv-Stream neu
zu starten — die 30s liefen sonst von vorne los.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verallgemeinert das "run_* → stumm"-Heuristik: jeder Skill deklariert im
Manifest ein speak-Flag.
- speak=false (Default) = Steuerbefehl (Spotify, Licht) → kein TTS, App
beendet direkt (STOP), wie bisher.
- speak=true = Antwort-Skill (Info/Ergebnis) → Antwort wird vorgelesen +
Gespraechs-Fenster bleibt offen.
Gilt für BEIDE Pfade: Fast-Path (_fast_path_speak aus skill.speak) und
lokale Skill-Ausführung (_local_turn_speak = _skill_speak_flag(run_*)).
Info-Tools (web_search/memory_search/trigger_timer) bleiben gesprochen.
- skills.py: speak in create_skill + update_skill-allowed.
- agent.py: skill_create/skill_update Tool-Schema dokumentiert speak (damit
ARIA es beim Skill-Bau setzen kann), Dispatch reicht es durch.
- App: _isSilent nur noch speak===false (kein answeredBy=fast-path-Fallback
mehr, sonst waere ein speak=true-Fast-Path faelschlich stumm).
Bestehende Skills ohne Feld = false = stumm (kein Verhaltenswechsel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fehlauslösung im Auto: Spotify läuft über die Lautsprecher, das Mikro hört
mit, openWakeWord halluziniert "computer" rein (Log: wake.detect state=armed
→ leerer Transkript). Der Echo-Canceler kann nur ARIAs eigenes TTS
rausrechnen, nicht Spotify (fremde App, kein Referenzsignal).
- Wake-Word-Threshold jetzt konfigurierbar (loadWakeThreshold), Default von
0.5 auf 0.6 hoch (strenger → weniger Fehlauslösung). Slider im Wake-Word-
Settings-Bereich (0.30–0.90, greift beim "Speichern + Aktivieren").
- Weiterreden-Fenster (passives Lauschen) als Slider, Default 30s (10–60s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nach dem Re-Arm-Fix rief die App resume() → das spielte einen zweiten Gong
und oeffnete ein neues Aufnahme-Fenster, obwohl Stefan nur einen Steuerbefehl
gab. Sein gewuenschtes Verhalten:
- Klarer Befehl (Fast-Path, speak=false, z.B. Liedersteuerung) = KEINE
Konversation → STOP: direkt zurueck aufs Wake-Word (kein Gong, keine
Aufnahme, kein 30s-Fenster).
- Gespraech (gesprochene Antwort) = kein neuer Gong, direkt 30s passives
Lauschen (weiterreden wie mit einem Menschen), 30s still → Wake-Word.
endConversation(skipPassive) neu: true springt direkt zu armed statt in
passives Lauschen. onPlaybackFinished (Gespraech) → passiv (Vordergrund) bzw.
direkt armed (Hintergrund); stille Fast-Path-Antwort → skipPassive; manueller
Stop-Button → skipPassive. resume() bleibt nur noch Mic-Fail-Retry.
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>
#2 Vorzeitiges Absenden: endpointMs war hart 1500ms — im Auto (Sprechpausen)
schnitt das mitten im Satz ab (die 11.8s-Frage wurde bei "…ohne dass ein"
gekappt). Jetzt konfigurierbar (aria_stt_endpoint_ms, Default 2400, 1000-4000).
#3 Ohr bleibt ausgegraut: beim Re-Arm rief der Wake-Word-Service
OpenWakeWord.start(), waehrend die passive Streaming-Aufnahme noch das Mikro
hielt → start() schlug fehl → state=off. Neuer micReleaseHook cancelt die
Aufnahme VOR start() (endConversation / exitPassiveListening /
discardIfFreshlyTriggered). ChatScreen registriert den Hook.
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>
Zeigt wie in Diagnostic, ob eine Antwort vom lokalen Modell / Claude / Fast-Path
kam — aber in der App als Opt-in, damit die App "Mama-tauglich" bleibt.
- ChatScreen: ChatMessage.answeredBy; aus dem chat-Payload UND der History-Sync
eingefangen; kleiner farbiger Badge im statusRow der ARIA-Bubble, nur wenn
showSource an. State aus AsyncStorage aria_show_source (default false, per
2s-Reload synchron mit den Settings).
- SettingsScreen: Toggle "Antwort-Quelle anzeigen" in der Chat-Bubbles-Card,
schreibt aria_show_source (pro Geraet — Stefan an, Mama aus).
- Bridge liefert answeredBy schon in chat_backup/History mit (kein Change noetig).
Server-Teil in 9cc1aec. APK-Rebuild noetig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Beide Restpunkte teilen eine Verkabelung: die projectId fliesst jetzt bis zum
Proxy und zurueck in die agent_activity-Events.
Restpunkt 1 — per-Kontext Activity-Indikator:
- Brain: proxy_client.chat_full(project_id) → payload.aria_project_id;
agent.py gibt active_project_id mit.
- Proxy routes.js: liest aria_project_id, taggt Tool-/Stream-Hooks damit,
trackt Subprozesse pro Kontext.
- Bridge: _emit_activity(project_id) + payload.projectId; /internal/agent-activity
reicht projectId durch; send_to_core/_process_core_response taggen thinking/idle
mit dem Turn-Kontext.
- App: agentActivityByCtx-Map; „ARIA denkt"-Indikator zeigt nur den
fokussierten Kontext statt global zu flackern.
Restpunkt 2 — kontext-scoped Cancel (Barge-In):
- Proxy: neuer /cancel {projectId} killt NUR die Subprozesse eines Kontexts
(_cancelByProject); /cancel-all bleibt fuers NOT-AUS.
- Bridge: soft cancel_request → _cancel_proxy_for_project(projectId) statt des
toten Diagnostic /api/cancel. Hard bleibt /cancel-all.
- App: cancel_request + Abbrechen-Button tragen die fokussierte projectId.
Damit laufen Kontexte in der App echt parallel: Arbeit in Projekt A wird durch
Senden/Abbrechen in Hauptchat oder Projekt B nicht mehr abgewuergt, und der
Indikator gehoert zum sichtbaren Chat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: waehrend ARIA in einem Projekt arbeitet, im Hauptchat eine Frage stellen →
sie wurde angezeigt aber nicht verarbeitet (erst wenn das Projekt fertig war).
Proxy kann nachweislich parallel (2 claude-Subprozesse) — der Flaschenhals war
die App: interruptAriaIfBusy wertete den GLOBALEN agentActivity aus und feuerte
bei jedem Senden-waehrend-irgendein-Kontext-arbeitet einen kontext-uebergreifenden
cancel_request.
Fix: Busy-Status kontextgenau aus queueStatus (/projects/queue-status) statt
global. Barge-In (Brain-Cancel) nur noch wenn GENAU der fokussierte Kontext
arbeitet; TTS wird weiterhin immer gestoppt wenn ARIA spricht. cancel_request
traegt jetzt die projectId (fuer spaeteres kontext-scoped Cancel in der Bridge).
Damit laufen Hauptchat und Projekt(e) in der App parallel — passend zum
Multi-Threading im Brain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: Bild/Datei ins Textfeld + Frage → beides landete im Hauptchat, egal
welches Projekt fokussiert war. Der Anhang-Pfad reichte die projectId nirgends
durch (im Gegensatz zum reinen Text-Pfad).
App (sendPendingAttachments):
- lokale Anhang-Bubble bekommt projectId (App-Focus)
- file-Upload (rvs.send('file')) schickt projectId mit
- Merge-Trigger chat-Nachricht schickt projectId mit
Bridge:
- file-Handler liest payload.projectId, merkt sie (_pending_files_project_id)
und taggt die Datei per _tag_file_to_project ins richtige Projekt
- _flush_pending_files_with_text(user_text, project_id): reicht die projectId
aus dem chat-Payload an send_to_core (Fallback: gemerkter Upload-Kontext)
- _flush_pending_files_after (Files-only): nutzt den gemerkten Upload-Kontext
- merged-Aufruf im chat-Handler gibt payload.projectId mit
Damit tragen User-Bubble, Datei-Manifest, Brain-Turn und ARIA-Antwort alle
denselben Projekt-Tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symptom: Frage im Projekt gestellt, Antwort nur in der App sichtbar, Frage nur
in der Diagnostic — Frage und Antwort scheinbar in verschiedenen Kontexten.
Ursache: die App taggt die lokale Voice-Bubble optimistisch mit dem App-Focus,
waehrend der Server (Voice-Router) die autoritative Zuordnung macht. Weichen
die ab (Registry-Race aus 3e88eec, Sticky, „fuer X:"-Prefix, Fallback), landet
die lokale App-Bubble in einem anderen Kontext als die serverseitig getaggte
Frage/Antwort → App- und Diagnostic-Ansicht divergieren.
Fix: beim Eintreffen des sender=stt-Broadcasts uebernimmt die App die
projectId des Servers fuer die Bubble (alle drei Match-Zweige). Nur wenn das
Feld mitkommt — altes Bridge-Format ohne projectId → App-Focus behalten.
Primaerursache bleibt 3e88eec (Router liefert wieder den korrekten Kontext).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kernbug: ProjectsBrowser.load() pushte bei JEDEM Drawer-Oeffnen status.active
in den App-Focus. Im Multi-Threading hat das Brain keinen globalen
active_project-State mehr → status.active ist null → Focus wurde auf Hauptchat
zurueckgesetzt. Folge: nach jedem Drawer-Oeffnen landeten alle Nachrichten im
Hauptchat, Projekte blieben leer.
- ProjectsBrowser: neues Prop currentFocusId (App-Focus = Source-of-Truth).
load() uebernimmt nur noch die Projektliste, kein onActiveChanged(status.active)
mehr. Highlight (✓ FOCUS) folgt currentFocusId.
- ChatScreen: currentFocusId={focusedProjectId} durchgereicht.
- ChatScreen: direkter „← Hauptchat"-Button im Focus-Header (nur im Projekt
sichtbar) — ein Tap statt Drawer→Hauptchat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zwei Bugs aus dem ersten Live-Test des Multi-Threading-Designs.
Bug 1 — Voice ignorierte App-Focus:
Stefan hat in Projekt X reingeguckt und was reingesagt — Message landete
im Hauptchat statt in X. Der Voice-Router auf der Bridge kannte den
sichtbaren Kontext der App nicht.
Fix:
- audio.ts.startStreamingRecording nimmt neuen opts.projectId und
schickt es im stt_stream_start-Payload mit.
- ChatScreen.tsx: alle 4 startStreamingRecording-Callsites (wake,
barge-in, passive, manuell) uebergeben focusedProjectIdRef.current.
Neuer useRef-Spiegel damit die Focus-ID auch in useCallbacks/
useEffects mit alten Closures aktuell bleibt.
- aria_bridge.py: neuer Handler fuer stt_stream_start speichert die
projectId in self._stt_stream_projects[requestId], stt_stream_end
loescht wieder. Beim stt_endpoint wird sie an _process_endpoint_text
weitergereicht und dort als default_project_id in den Voice-Router.
- _apply_voice_router bekommt neuen Prio-Rank 4: „App-Focus als
Default" — greift wenn kein Meta, kein Prefix und kein aktiver Sticky.
So folgt Voice ohne extra Marker dem sichtbaren Kontext.
Bug 2 — Back-to-Main-Regex zu eng:
„zurück ins hauptmenü" wurde nicht als Meta erkannt (Regex matchte nur
„zurück zum hauptchat") und landete deshalb im aktiven Sticky-Projekt.
Fix: Regex akzeptiert jetzt auch hauptmenü, menü, haupt, main mit
Praepositionen „zum/zur/ins/in den".
Bonus — Burger-Button heller:
Stefan konnte den ☰-Toggle im Header kaum sehen. Farbe von Default
(dunkelgrau) auf #E0E0F0 (hell) mit fontWeight 700 gesetzt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 2 vom Multi-Threading-Redesign. Chat zeigt jetzt genau EINEN Kontext
(Hauptchat oder Projekt X) — die anderen laufen im Brain weiter, sichtbar
nur ueber Status-Dots im Drawer.
ChatScreen:
- Reorder-Trick + collapsible Project-Bloecke raus. messagesForRender filtert
jetzt direkt auf focusedProjectId.
- Neuer Focus-Header oben: ☰ Drawer-Toggle + Kontext-Name + Status-Dot
(gruen idle / gelb queue / rot arbeitet). Drawer-Icon kriegt ein Badge
mit der Anzahl OTHERE aktiver Kontexte.
- Focus in AsyncStorage gespiegelt — Neustart restauriert den letzten Blick.
- brainApi.getProjectQueueStatus() alle 2s gepollt fuer Status-Dots.
- project_changed-Event steuert Focus-Wechsel (App-lokal, kein Brain-Roundtrip).
brainApi:
- Neuer Typ QueueContextStatus + ProjectQueueStatus.
- Methode getProjectQueueStatus() → /projects/queue-status.
ProjectsBrowser:
- Nimmt queueStatus als Prop, rendert Status-Dot pro Zeile (Hauptchat +
Projekte).
- switchTo ruft NICHT mehr brainApi.switchProject (kein globaler active
mehr) — direkt onActiveChanged mit dem Projekt-Objekt aus der Liste,
schliesst danach die Modal.
- Label ✓ FOCUS statt ✓ AKTIV — praeziser fuer's neue Modell.
SettingsScreen:
- File-Manager-Filter-Default nutzt AsyncStorage statt Brain-Query.
Bewusst nicht drin (Follow-up):
- OS-Push wenn Projekt fertig ist — braucht Firebase-Setup, kommt separat
wenn die visuellen Dots nicht reichen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Zwei Stefan-Reports nach dem ersten Live-Test:
1. App-Reload verlor die Projekt-Bloecke
- chat_backup.jsonl hatte keine project_id-Felder, also kamen die
Bubbles als Hauptchat zurueck wenn die App ueber chat_history_response
ihre History neu lud.
- Fix: aria_bridge schreibt jetzt project_id in jeden Backup-Eintrag.
Assistant-Reply via turn_pid (aus ChatOut.project_id); User-Message
via payload.projectId (oder Brain-Status-Query als Fallback fuer
Trigger-Replies / Diagnostic-Sends).
- App: chat_history_response-Mapper liest m.project_id → ChatMessage.projectId.
2. Raus + rein in ein Projekt erzeugte einen zweiten Block am Ende
- Vorher: Gruppierung bei aufeinanderfolgenden gleich-getaggten Bubbles.
Hauptchat dazwischen hat den Block "unterbrochen", neuer Block.
- Fix: neue reorderedMessages-Stufe sortiert Messages so um, dass alle
eines Projekts contiguous werden, verankert am LATEST-Activity-
Timestamp des Projekts. Genau EIN Block pro projectId — bei
Re-Enter wandert der existierende Block ans Zeitende der Liste,
die neue Bubble haengt unten in der Gruppe.
- Hauptchat-Bubbles bleiben chronologisch zwischen den Projekt-Blöcken.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drei Stefan-Bugs aus dem ersten Deploy-Test plus die fehlenden Polish-
Features fuer die Projekt-Funktion.
Fixes:
- ProjectsBrowser-Spinner-Hang: useRef-Pattern statt useCallback([onActive
Changed]) — Parent uebergibt inline-arrow-Callbacks, neue Identitaet
jedes Render → useCallback recomputes → useEffect refeuert → infinite
Spinner. Fix: Ref-Bridge fuer Callbacks, useCallback mit empty deps.
- ChatScreen Banner: zusaetzlicher × Hauptchat-Button rechts (sichtbar
nur wenn Projekt aktiv) — ein Tap und zurueck zum Hauptthread, ohne
Modal-Umweg.
Features:
- Brain ChatOut.project_id: aktive Projekt-ID NACH dem Turn (kann
durch project_enter/exit-Tools waehrend Turn gewechselt sein). Bridge
liest sie aus dem /chat-Response und haengt sie an jeden ARIA-Chat-
Broadcast als payload.projectId.
- App: ChatMessage.projectId-Feld. User-Bubbles werden mit aktiver
Projekt-ID getaggt vor dem Senden (auch im RVS-Payload). ARIA-Bubbles
kriegen die ID vom Bridge.
- App: Chat-Verlauf rendert aufeinanderfolgende Project-Messages als
einklappbaren Block mit Header (▶/▼ + Projekt-Name + Count). Auto-
Collapse beim Projekt-Wechsel (altes ein, neues aus), Default beim
ersten Render: alle inaktiven Projekte eingeklappt.
- File-Manager Project-Tagging:
- diagnostic/server.js: Manifest /shared/config/file_projects.json
+ /api/files-list returnt projectId pro Datei + neuer Endpoint
/api/files-set-project.
- bridge/aria_bridge.py: nach App-Upload Auto-Tag mit aktivem Projekt
(Brain-Status-Query, best-effort fail-silent).
- App SettingsScreen: scrollbare Projekt-Pill-Reihe als Filter, default
auf aktives Projekt wenn vorhanden, sonst "Alle Projekte".
- Diagnostic: zweites Dropdown im Files-Tab, baut Projekt-Optionen
dynamisch aus /api/brain/projects/list.
Bewusst nicht drin (Folgeschritt):
- Per-File "Projekt zuweisen"-Action (Long-Press / Right-Click)
- Filter-Sync zwischen ChatScreen-Banner und SettingsScreen-Filter
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Projekte sind benannte Thema-Bündel die voice-gesteuert via Brain-Tools
geöffnet/verlassen werden. Default-Mode bleibt der Hauptthread — Projekte
sind eine optionale Bühne. Anchored-not-replaced: App-Open landet immer
im Hauptchat, Projekte sind nur sichtbar wenn aktiv betreten.
Brain:
- projects.py: CRUD + Fuzzy-Find + Active-State-Pointer
(/shared/config/projects.json + active_project.txt).
- conversation.py: Turn.project_id-Feld + window(project_id) Filter.
- agent.py: 6 Meta-Tools — project_create / _enter / _exit / _list /
_summary / _end. chat() liest aktive Projekt-ID, taggt User+Assistant-
Turns damit, filtert das LLM-Window auf Projekt-Kontext und ergaenzt
den System-Prompt um den aktiven Projekt-Hinweis. touch_project pflegt
last_activity_at + turn_count.
- main.py: REST-Endpoints /projects/{status,list,create,switch,
{id}/end,{id}/archive, PATCH /{id}}.
Bridge + RVS:
- aria_bridge.py: project_changed Event-Propagation Brain → RVS-Broadcast
damit App + Diagnostic ihre Banner refreshen.
- rvs/server.js: project_changed in ALLOWED_TYPES.
App:
- brainApi.ts: Project-Type + 6 API-Methoden.
- ProjectsBrowser.tsx (neue Komponente, ~340 Zeilen): Status-Header,
Hauptchat als Erster-Eintrag, Projekt-Liste mit Aktiv-Marker, Long-Press
zum Editieren, Modals fuer Neu/Edit/End/Archiv.
- ChatScreen.tsx: Banner unterhalb des Status-Bars zeigt aktives Projekt
oder „Hauptchat" — Tap öffnet ProjectsBrowser als Modal. Aktive Projekt-
Info wird bei Mount + bei project_changed-Events refreshed.
- SettingsScreen.tsx: Neue Section 📁 „Projekte" zeigt ProjectsBrowser inline.
Diagnostic:
- Neue Sektion im Brain-Tab mit Liste, Aktiv-Marker, Beenden/Archivieren
pro Zeile, Modal fuer Neu. Lädt automatisch bei Brain-Tab + bei
project_changed-Event-Broadcast.
Was bewusst NICHT drin ist (Folgeschritte):
- Per-Message Filter im Chat-Verlauf (zeigt aktuell alle Bubbles, Banner
zeigt Kontext) — App müsste Chat-History per project_id filtern.
- Files-by-Project Tagging.
- Inline-Collapse-Bloecke im Chat-Verlauf.
- Sub-Projekte (Stefan-Entscheidung: weglassen, „Mama-tauglich").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neuer State 'listening' im WakeWordService. Nach endConversation faellt
ARIA nicht direkt zu armed zurueck, sondern ins passive Lauschen fuer
PASSIVE_LISTEN_DEFAULT_MS (Default 30s, in AsyncStorage konfigurierbar).
In dem Fenster braucht Stefan kein Wake-Word mehr — er kann einfach
weitersprechen, Speaker-ID-Gating in der Whisper-Bridge filtert fremde
Stimmen (TV, Frau, Hintergrundgespraeche).
Flow:
armed → wake → conversing → TTS → resume → (Nichts gesagt) →
endConversation → enterPassiveListening('listening' + Timer) →
startPassiveStreamingRecording (kein User-Bubble, kein wake-ready-Sound)
→ Speaker-ID-Gating in Bridge → Speech detected:
exitPassiveListening('speech') → 'conversing' → normaler Flow
→ Nichts in N Sek:
Timer feuert → exitPassiveListening('timeout') → 'armed' (Wake an)
Implementation:
- wakeword.ts: WakeWordState += 'listening'. enterPassiveListening +
exitPassiveListening + onPassiveListen-Callback + Cancel-Timer-Hooks
in stop(). PASSIVE_LISTEN_DEFAULT_MS/STORAGE_KEY + load/save Helpers.
- ChatScreen.tsx: state-Type um 'listening' erweitert. State-Listener
schliesst Conversation-Focus auch in 'listening' (Spotify bleibt
pausiert). onPassiveListen → startPassiveStreamingRecording mit
noSpeechTimeoutMs=passiveMs. STT-Endpoint-Handler: bei text != ''
und state=='listening' → exitPassiveListening('speech'); bei
text == '' und state=='listening' → naechste passive Aufnahme.
Beim Wechsel listening→armed/off: laufende streaming-Aufnahme
cancellen damit OpenWakeWord beim Re-Arm das Mic kriegt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
App-Seite:
- VoiceIdEnrollment.tsx (neue Komponente, ~370 Zeilen): Status-Karte
(loading/unenrolled/enrolled/error), Sample-Recorder mit Countdown
(4s fest pro Sample), Liste mit einzelnem Loeschen, Save-Button
(disabled bis 5 Samples), Fingerprint-Delete mit Confirm.
- SettingsScreen.tsx: neue Section 🎤 'Stimme einrichten' zwischen
Wake-Word und Sprachausgabe.
- Sample-Format: WAV via audioService.startRecording — wird
whisper-bridge-seitig per wave-Modul gestrippt.
Diagnostic-Seite:
- Neue settings-section 'Voice-ID (Sprecher-Erkennung)': Status-Anzeige
(live ueber voice_id_status_response), Threshold-Slider 0.30-0.70
(persistiert in voice_config.json, broadcast als config-Message),
Refresh + Delete-Button.
- server.js: 2 neue actions (voice_id_status, voice_id_delete),
send_voice_config nimmt voiceIdThreshold mit auf.
Backend:
- speaker_id.py: _normalize_audio_bytes erkennt jetzt WAV-Header
(RIFF/WAVE) und strippt auf rohes PCM — sonst werfen die ECAPA-
Embeddings auf den 44-Byte-Header rein.
- bridge.py: config-Broadcast-Handler setzt voiceIdThreshold auf
speaker_id.DEFAULT_THRESHOLD (wird erst in Phase 3 beim Gating
genutzt, persistiert aber schon).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- WakeWord Doppel-Trigger: detectionInProgress-Guard gegen Native-Event-
Race + setBackground/setForeground statt setResumeCooldown im AppState.
- Media-Pause beim App-Oeffnen: 1.5s Startup-Suppression im Kotlin
emitDetected() — Mikro-Spin-up-Spike triggert kein false-positive mehr.
- Spotify Fast-Path im Brain: einfache Media-Commands (naechster Track,
pause, play, lauter, ...) matchen via Regex und gehen direkt aufs
spotify-Skill statt durch Claude. ~1.5s statt 5-10s pro Befehl.
- File-Limit auf 1 GB hochgezogen (war 70 MB). RVS maxPayload +
Bridge max_size auf 1500 MB; Node-Heap im RVS-Container auf 4 GB.
- TriggerBrowser / Datei-Manager Connection-Refused: brainApi._send
fast-failt bei disconnected RVS statt 30s zu timeouten, und beide
UIs reloaden automatisch beim Reconnect-Event.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stefan's UX-Wunsch: Datei direkt oeffnen ohne Umweg ueber Download.
Plus: in der App fehlte komplett der Per-Row-Download-Button (nur via
Checkbox + Bulk-Download). Beides jetzt gefixt.
App (SettingsScreen.tsx):
- Neue per-Row-Buttons: 👁 Open + ⬇ Download + 🕒 Versionen + 🗑 Loeschen
- Open-Pfad nutzt requestId-Praefix 'open-' im file_response-Handler
→ Datei wird nach CachesDirectory geschrieben (kein Storage-Bloat)
→ FileOpener-Native-Module (Intent.ACTION_VIEW mit MIME) oeffnet
mit dem System-Picker → User waehlt PDF-Viewer / Galerie / Player
- guessMimeFromName-Helper fuer den Intent damit Android die passende
App findet
- Download-Pfad unveraendert ('single-' Praefix), schreibt nach
DownloadDirectory mit Suffix-Inkrement bei Namens-Konflikt
Diagnostic (server.js + index.html):
- Neue Route /api/files-view (gleicher Code-Pfad wie files-download,
aber Content-Disposition:inline + echter MIME-Type statt octet-stream)
- Browser zeigt PDF / Bilder / Text im neuen Tab statt forcierten Download
- 👁-Button in jeder File-Row neben ⬇/🕒/🗑
- Fallback fuer unbekannte MIMEs: octet-stream → Browser bietet Download
Bei beiden Pfaden bleibt der Cache nutzbar: nach App-Open kann der User
die Datei im jeweiligen Viewer behalten; im Browser bleibt sie im Tab.
Step 3 vom File-Versioning-Feature (Step 1+2 lief schon in Diagnostic).
App kann jetzt:
- pro Datei via 🕒-Button die Versions-Liste anzeigen
- alte Versionen als '<name>@<short-hash>.<ext>' nach Downloads schreiben
- per ⟲ eine alte Version als neue aktive setzen (non-destructive,
macht im Backend einen 'restore:'-Commit, die bisherige Version
bleibt in der Historie)
Drei neue RVS-Message-Type-Paare:
file_version_list_request / _response
file_version_download_request / _response (base64)
file_version_restore_request / _response
rvs/server.js: alle sechs Typen in die ALLOWED_TYPES-Whitelist.
bridge/aria_bridge.py: handler proxen die Anfragen an diagnostic
(http://localhost:3001/api/files-versions / -version-content / -restore).
Diagnostic ist eh schon der Owner der git-Repository-Logik. Bridge
wrappt die Binary-Antwort als base64 fuer den RVS-Transport.
android/src/screens/SettingsScreen.tsx:
- State versionsOpen/versionsList/-Loading/-Error
- Drei rvs.onMessage-Branches fuer die neuen *_response Types
- 🕒-Button in jeder Datei-Zeile (zwischen Auswahl-Checkbox und Mülltonne)
- Neues Modal mit Versions-Liste (AKTIV-Badge, short-hash, subject,
formatiertes Datum, ⬇ Download + ⟲ Restore-Button pro Eintrag)
- Restore-Button hat Confirm-Alert
- Bei file_version_restore_response: list refresh + file-list refresh
Bug: '⬇ Download' im Dateimanager schickte file_request raus, aber kein
SettingsScreen-Handler nahm das file_response auf. ChatScreen fing es
zwar global ab, versuchte aber nur Chat-Bubble-Attachments zu
patchen — kein Match, also passierte sichtbar nichts.
Fix: Handler in SettingsScreen fuer file_response mit requestId-Praefix
'single-' (aus bulkDownload-1-Datei-Pfad). Schreibt nach
RNFS.DownloadDirectoryPath, mit Suffix-Inkrement bei Namens-Konflikt
damit nichts ueberschrieben wird.
Multi-Datei-Download (ZIP) lief schon ueber file_zip_response,
unangetastet.
Symptom: Wake-Word laeuscht nach erfolgreicher Konversation im
Hintergrund nicht wieder — erst beim App-Vorholen wird's wieder
armed. Grund: nach TTS-Ende laeuft wakeWordService.resume() in
einen setTimeout(800ms) der im Doze stark verzoegert wird. Der
verspaetete Timer findet dann delay > 2800 und ruft endConversation
(re-arm) — aber eben erst beim App-Resume.
Fix: in onPlaybackFinished AppState pruefen:
active → resume() wie bisher (Multi-Turn-Conversation-Window)
background → endConversation() direkt — kein setTimeout, native
OpenWakeWord.start() greift sofort.
Begruendung fuer das Verhalten:
- Foreground: User ist aktiv, Multi-Turn-Dialog ohne erneutes
"Computer"-Sagen ist nuetzlich.
- Background: User nutzt das Handy anderweitig, automatisches Mikro-
Oeffnen ist nicht erwartet und droht durch Doze-Verzoegerung in
ein Phantom-Trigger-Mismatch zu kippen. Direkt re-armen ist
robust + erwartungskonform.
Eng verwandt mit dem 0.1.7.0-Fix (kein setTimeout zwischen
wake.detect und Callback) — selbes Doze-Throttling-Pattern, andere
Stelle in der Pipeline.
VoiceButton rewrite — dB/VAD-Pfad endgueltig raus. Knopf ist jetzt nur
noch UI-Trigger:
- onTapStart (ChatScreen baut Bubble + startStreamingRecording)
- onTapStop (ChatScreen ruft stopStreamingRecording)
- audioService.onStateChange treibt die Animation (statt internem
isRecording-Flag)
- onSilenceDetected-Subscription weg
ChatScreen:
- handleVoiceRecording (Legacy) → handleVoiceButtonStart +
handleVoiceButtonStop
- Bubble wird beim Tap SOFORT gebaut (vorher: erst nach Stop), Text
landet via audioRequestId-Match im chat-Handler-Update-Pfad
- noSpeechTimeoutMs=0 (manueller Modus, User kontrolliert via Tap),
hardCapMs=300_000 (5 Minuten Notbremse)
- Wake-Word-conversing + manueller Stop = endConversation (User
will nicht in Multi-Turn-Modus)
- RecordingResult-Import entfaellt (nicht mehr genutzt)
Damit ist die komplette App-seitige Aufnahme auf Streaming + ML-
Endpointer. Der ganze dB/VAD-Apparat (vadEnabled, vadBaselineSamples,
loadVadSilenceDbOverride, vadTimer, noSpeechTimer, etc.) ist jetzt
nur noch Dead-Code — wird in einem Folge-Commit gemeinsam mit dem
zugehoerigen Settings-Slider abgeraeumt.