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>
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>
- 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>
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.
audio.ts:
- neue Methoden startStreamingRecording / stopStreamingRecording /
cancelStreamingRecording mit PcmStreamRecorder als AudioRecord-Source
- permanenter RVS-Listener fuer stt_partial / stt_endpoint / stt_stream_done,
Filterung ueber streamRequestId-Match
- Callbacks onSttEndpoint(SttEndpointEvent) + onSttPartial(text)
- No-Speech-Watchdog + App-seitiger Hard-Cap (+2s Toleranz gegen Bridge)
- cancelStreamingRecording feuert onSttEndpoint mit text='' damit
ChatScreen den No-Speech-Fall behandeln kann (wie frueher
onSilenceDetected -> stopRecording() -> null)
- Legacy startRecording / stopRecording / onSilenceDetected unangetastet
-- VoiceButton (manuelle Aufnahme) nutzt das weiterhin
ChatScreen.tsx:
- Wake-Callback: startRecording -> startStreamingRecording
- Bubble wird sofort gebaut, audioRequestId landet via
stt_endpoint -> chat(sender=stt) im chat-Handler-Update-Pfad wie bisher
- onSilenceDetected entfernt, ersetzt durch onSttEndpoint:
text != '' -> log, aria-bridge triggert Brain selbst (Phase-2-Shortcut)
text == '' -> endConversation (No-Speech-Fall)
- Barge-In via Wake-Word: ebenfalls auf Streaming umgestellt
- AppState-resume + toggleWakeWord-off pruefen jetzt isStreamingRecording()
und nutzen passenden Cancel
Damit: kein dB/VAD mehr im Hot-Path. Whisper hoert auf semantische
Stille (kein neuer Text), Brain bekommt den Text direkt von aria-bridge,
Audio-Roundtrip App->aria->whisper->aria->App entfaellt komplett.
Stefan-Anforderung: Background-Wake-Word-Pipeline klappt noch nicht,
ADB nicht zur Hand → Debug via RVS-Log-Pipeline.
Logger:
- reportAppDebug(scope, message) analog zu reportAppError aber
level=info, kein console.error, fuer Live-Diagnose
Strategische Log-Punkte:
- wakeword.ts: start() emits 'wake.start armed'
- wakeword.ts: onWakeDetected emits 'wake.detect state=X' beim
Native-Trigger-Empfang
- ChatScreen.tsx wake-callback: 'wake.cb callback fired',
'wake.cb startRecording=X', 'wake.cb gong played'
- backgroundAudio.ts: 'bg.start slot=X', 'bg.stop service stopped',
'bg.start.fail msg' wenn Service nicht hochkommt
Abruf live via curl http://172.0.2.33:3001/api/app-log?lines=100
Damit kann Stefan nach APK-Build (mit allen Native-Fixes + Logger)
im Background-Test exakt sehen wo es klemmt:
- Kommt 'wake.detect' im Hintergrund an? (WakeLock-Frage)
- Kommt 'wake.cb callback fired'? (JS-Bridge-Frage)
- Geht 'bg.start slot=wake' durch? (Service-Start-Frage)
APK neu bauen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
App-Bugs:
- Trigger-Liste war leer: brainApi.listTriggers() cast'te {triggers: [...]}
direkt als Array, t.sort() warf — TriggerBrowser blieb leer. Fix: unwrap.
- GPS-Tracking startete erst bei SettingsScreen-Mount, nicht beim App-Boot.
Wenn Stefan direkt in den Chat ging, blieb GPS aus. Fix: restoreFromStorage()
in App.tsx useEffect.
- Text in Chat-Bubbles nicht markierbar / kein Copy-Mechanismus: Bubble jetzt
Pressable mit onLongPress + neues ⎘-Icon in Status-Row → openBubbleActions().
Alert-Menu mit "Ganzen Text teilen" + pro extrahierte URL/Mail/Tel eine
eigene Option. Share.share() — keine neuen Native-Deps noetig.
Brain — Skill-Mgmt:
- ARIA legte beim Skill-Umbau neue Versionen mit Suffix an (Skill-Friedhof),
weil sie kein Update/Delete-Tool kannte. Zwei neue META_TOOLS in agent.py:
skill_update (kann entry_code, readme, pip_packages, args, description,
active patchen — venv wird bei pip_packages-Aenderung rebuilt) + skill_delete.
- skills.py update_skill um entry_code/readme/pip_packages erweitert,
venv-Rebuild bei pip-Aenderung.
Bridge — Voice-Speed persistent:
- _next_speed_override war pro-Request-Override ohne Persistenz. Bei
Diagnostic-Chats / Trigger-Replies ohne vorherigen App-Chat fiel der Speed
auf 1.0 zurueck, ebenso nach Bridge-Restart. Jetzt: _persistent_xtts_speed
aus voice_config.json (xttsSpeed), wird nach jedem App-chat mit speed
autopersistiert. TTS-Generation faellt zurueck: per-Request > persistent > 1.0.
App — Feature 6:
- SkillBrowser.tsx: Liste aller Skills, Toggle aktiv/inaktiv, Detail-Modal
mit Args-Inputs, Ausfuehren mit Live-stdout/stderr, Logs der letzten 20
Runs, Loeschen. Settings-Sektion "Skills" (🛠️) zwischen Trigger und
Protokoll. brainApi.listSkills/getSkill/runSkill/updateSkill/deleteSkill/
getSkillLogs ergaenzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnostic-Einstellungen fuer FLUX:
- Default-Modell (dev | schnell) — wird via RVS gepusht, flux-bridge
hot-swappt die Pipeline aus dem HF-Cache (~15-30s)
- Raw-Keyword (Default 'flux') — Pipe-Modus, Brain leitet Stefans Text
1:1 als prompt durch, kein Rewriting/Beautify
- Switch-Keyword (Default 'fix') — zwingt das ANDERE Modell als Default
Brain-Tool flux_generate um model + raw erweitert, System-Prompt-Block
mit den aktuellen Diagnostic-Settings + Whisper-Toleranz-Hinweis.
Kein eager Bootstrap-Load: flux-bridge wartet auf config oder ersten
Request. Bei erstem HF-Download zeigt Banner "laedt erstmalig runter"
mit Pfeil-Icon, Toast in der App wenn fertig.
FLUX_MODEL aus der .env entfernt (Steuerung jetzt komplett ueber
Diagnostic). HF_TOKEN-Kommentar erklaert warum trotz lokaler Inference
noetig (HF Gate-Mechanismus fuer FLUX.1-dev).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom (aus Bridge-Log): bei chat_history_request triggert die App
file_request fuer alle fehlenden Anhaenge. Bei einem 40 MB MP4 wird das
base64-encoded ~53 MB, ueberschreitet das RVS-maxPayload (50 MB).
Server droppt mit Code 1009 'message too big', Bridge crasht im cleanup
mit AttributeError 'NoneType has no call_soon' (websockets-Lib-Bug bei
nested context-manager-cleanup nach abgerissener Verbindung).
Drei Layer:
(1) RVS-Server: maxPayload 50 → 100 MB — deckt ~70 MB binaer ab nach
base64-inflate. Comment im server.js erklaert den Hintergrund.
(2) Bridge: max_size 50 → 100 MB synchron zum Server. PLUS pre-check
im file_request-Handler — Dateien > 70 MB werden mit Fehler-Response
abgewiesen statt blind base64-zu-encoden und die WS zu killen.
Limit knapp unter Server-Limit damit Bridge proaktiv blockiert.
(3) App: file_response-Handler liest 'error'-Feld aus dem Payload und
zeigt nen Toast 'Datei X: Datei zu gross fuer Transfer (40 MB,
Limit 70 MB)'. Statt einfach zu schweigen oder endlos zu retryen.
Crash bei websockets-cleanup ist ein Lib-Bug (NoneType.call_soon) —
nicht direkt fixbar, aber tritt jetzt nicht mehr auf weil Bridge proaktiv
die zu grossen Files ablehnt und die WS nicht mehr abreisst.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bridge fuegt User-Texten Praefixe in eckigen Klammern hinzu damit Brain
Kontext hat — z.B. '[Stefans aktuelle GPS-Position: 53.0, 8.5. Nutze die
nur wenn ...]' oder '[Hinweis: Stefan hat dich gerade unterbrochen...]'.
Die landeten via chat_backup auch in der App-Bubble — Stefan sieht jeden
Hint mit, hat nichts in der UI verloren.
Fix: App-side stripSystemHints() filtert aufeinanderfolgende `[...]`-
Bloecke am Textanfang inkl. Trennleerzeichen. Wird in renderMessage
angewendet, default an (Hints versteckt). Toggle in Settings →
Allgemein → 'Chat-Bubbles' kehrt's um falls Debug gewuenscht.
Brain bekommt weiterhin den vollen Text — Bridge-Side unveraendert.
Live-Toggle: Settings setzt aria_show_hints in AsyncStorage, ChatScreen
re-liest alle 2s (gleicher Mechanismus wie tts_enabled etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: App bekommt im minimierten oder display-gesperrten Zustand
nicht mit ob ein Anruf angefangen oder beendet wurde — TTS spricht
weiter waehrend Telefon klingelt, oder bleibt stumm nach Auflegen.
Zwei Ursachen:
1) Kotlin: TelephonyCallback war auf reactApplicationContext.mainExecutor
registriert. Wenn die Activity pausiert ist (display aus, App im
Hintergrund), wird der mainExecutor verzoegert oder gar nicht
abgearbeitet — Call-State-Events kommen nicht durch.
Fix: eigener Executors.newSingleThreadExecutor() — laeuft unabhaengig
vom UI-Thread solange der App-Prozess lebt (Foreground-Service
garantiert das).
2) TS: TelephonyManager-Listener kann nach laengerer Hintergrund-Zeit
verloren gehen (React-Bridge-Context recreated nach Resume).
Fix: neue refresh()-Methode in phoneCallService, AppState-Resume
ruft sie auf — wenn telephonyAttached=false ist, wird der Native-
Listener neu attached.
Plus: Status-Property telephonyAttached macht in Logs sichtbar ob
Pfad 1 (TelephonyManager) wirklich greift. Pfad 2 (AudioFocus fuer
VoIP) war nie betroffen, der laeuft komplett im Native-Code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: Ohr aktiv, App im Hintergrund (jetzt mit Foreground-Service
permanent lebendig), nach laengerer Zeit oeffnet Stefan die App und sie
nimmt schon auf — angeblich Wake-Word getriggert. War aber TV/Husten/
sonstige Hintergrund-Geraeusche waehrend Stefan nicht da war.
Mit dem neuen Hintergrund-Modus laeuft openWakeWord jetzt permanent und
faengt jedes False-Positive im Hintergrund auf. Ohne dieser Fall war
das nicht moeglich weil die JS-Engine pausiert war.
Fix: Heuristik beim AppState-Resume in ChatScreen.tsx
- backgroundDauer wird gemerkt (lastBackgroundAt vs Resume-Zeit)
- Wenn >30s im Hintergrund UND state='conversing' UND letzter Wake-
Trigger juenger als 15s: false-positive — Aufnahme abbrechen + zurueck
zu armed
- Resume-Cooldown 1500 → 3000 ms (Audio-Spikes beim AppState-Switch
haben gelegentlich nach 1.5s noch nicht verklungen)
Neue Methoden:
- wakeword.ts: lastTriggerAt-Tracking + discardIfFreshlyTriggered(maxAge)
- audio.ts: cancelRecording() — bricht recorder ab ohne Result zu
emittieren, loescht die Audio-Datei
Setzt voraus dass Stefan nicht laenger als 30s im Hintergrund mit ARIA
spricht ueber Wake-Word. Falls doch: bei Resume waere die Aufnahme weg
und er muesste nochmal triggern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(1) Gedanken-Stream Modal: vorheriger Fix mit onStartShouldSetResponder
war falsch — der View wurde komplett zum Responder, die FlatList drin
bekam null Touch-Events. Jetzt: outer View ohne Touch-Handling, ein
separates TouchableOpacity-Element oberhalb des Sheets nur fuer den
Tap-Outside-Close. Sheet-View ist plain View → FlatList scrollt frei.
(2) Such-Sprung praeziser: drei Verbesserungen
- MAX_SCROLL_RETRIES 3 → 6: bei weiten Spruengen (Bubble #150 von
Position 0) braucht FlatList mehrere Iterationen bis die Items in
der Naehe gemessen sind
- Pre-Scroll-Offset: Fallback fuer unmeasured Items ist jetzt der
dynamische Mittel der bisher gemessenen Items (statt Pauschal-150).
Beim Cold-Start sind nur die untersten 10 gemessen, aber deren
Mittel ist immer noch eine bessere Schaetzung
- Render-Pause nach Pre-Scroll 200 → 350 ms: bei weiten Spruengen
braucht FlatList Zeit die Items zu mounten und onLayout zu feuern
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
App-Inbox-Modal:
- ScrollView der Top-Section ('Aus diesem Chat') nestedScrollEnabled=true
- MemoryBrowser darunter in einen flex:1-Wrapper gepackt damit er den
verbleibenden Platz bekommt — ohne den hat seine FlatList intern
null Hoehe gehabt und Scroll-Gestures verschluckt.
Diagnostic Trigger-Tab:
- ✎ Bearbeiten-Knopf pro Zeile (neben Aktivieren/Deaktivieren/Loeschen)
- Modal hat jetzt einen Edit-Modus: Type+Name disabled, Save-Button
zeigt 'Speichern', Modal-Title 'Trigger bearbeiten — <name>'
- Fuer Timer im Edit-Modus ein zusaetzliches Feld 'Feuert am (ISO, UTC)'
damit man den absoluten Zeitpunkt direkt aendern kann (statt 'in X
Minuten ab jetzt' das nur fuer Create Sinn macht)
- saveTrigger() unterscheidet jetzt zwischen Create-Modus (POST
/triggers/timer|watcher) und Edit-Modus (PATCH /triggers/{name})
- openTriggerEdit(name) fuellt das Modal mit Werten aus dem Cache
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(1) Such-Treffer jetzt neueste zuerst (analog WhatsApp/Telegram). User
ist visuell unten, der erste Sprung landet meist im Viewport ohne
weiten Pre-Scroll (= weniger Cold-Start-Fail-Risiko). „Naechster"
geht in die Vergangenheit. Plus Pre-Scroll-Wartezeit 80→200 ms damit
FlatList beim ersten Versuch wirklich Zeit zum Rendern hat.
(2) SettingsScreen Ueber-Text: `—` wurde literal gerendert weil
JSX-Text-Knoten keine JS-String-Escapes interpretieren. Fix:
`{'—'}` als JS-Expression-Block.
(3) GPS-Tracking sendete nach der initialen Position nichts mehr wenn
der User stationaer war — `distanceFilter: 30` blockiert
watchPosition-Updates ohne Bewegung. Nach 5 min (NEAR_MAX_AGE_SEC)
verwirft das Brain die Position als veraltet → near()-Watcher feuern
nie. Stefan's DRK-Trigger waren so chronisch tot.
Fix: zusaetzlich zum watchPosition laeuft ein setInterval(60s)
Heartbeat der die zuletzt empfangene Position erneut sendet. Kein
extra GPS-Wakeup — akkufreundlich. Damit bleibt der Brain-State
frisch auch bei stationaerem User; near() funktioniert sobald der
User tatsaechlich im Radius ist.
Anmerkung zu Stefan's konkretem Test: er war 1.5–2 km von den DRK-
Triggern entfernt (Radius je 300 m) — selbst mit frischen GPS-Updates
haetten die nicht gefeuert. Der Heartbeat-Fix ist trotzdem noetig
damit Trigger ueberhaupt eine Chance haben wenn er tatsaechlich dort
vorbeifaehrt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: Suche nach 'cessna' sprang zur Oberhausen-Bubble (~15 Bubbles
daneben), egal welcher Versuch.
Zwei Ursachen:
1) searchMatchIds suchte in `messages` (alle Bubbles inkl. Memory/Skill/
Trigger-Spezial-Bubbles), aber gescrollt wird in `invertedMessages`
die diese filtert. Wenn 'cessna' nur in einer Memory-Bubble vorkam,
war die ID in searchMatchIds aber nicht in invertedMessages →
findIndex=-1 → kein Scroll, Pre-Scroll-Offset von voriger Aktion
blieb sichtbar. Fix: searchMatchIds aus chatVisibleMessages.
2) AVG_BUBBLE_HEIGHT=150 als Pauschalschaetzung war zu grob — Voice-
Bubbles sind ~70 px, lange ARIA-Antworten 400+. Pre-Scroll-Offset
landete bei langen Listen weit daneben. Fix: itemHeights-Ref-Map
wird per onLayout in renderMessage gefuettert. Pre-Scroll summiert
echte gemessene Hoehen (Fallback AVG fuer noch nicht gerenderte) —
beim zweiten Such-Versuch lernt der Cache, beim ersten klappt's
schon besser als mit dem Pauschalwert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: Suchbegriff direkt nach App-Start eingegeben → springt an
falsche Stelle. Erst beim zweiten Versuch funktioniert es.
Ursache: FlatList rendert per Default nur 10 Items initial.
info.averageItemLength im onScrollToIndexFailed basiert nur auf diesen
10 — bei einem Suchtreffer auf Bubble 150 ist die Schaetzung katastrophal
falsch. Beim zweiten Versuch ist die FlatList „warm gelaufen" und mehr
Items sind gemessen → Schaetzung passt besser.
Drei kombinierte Fixes:
1) Pre-Scroll: vor dem scrollToIndex erst grob mit AVG_BUBBLE_HEIGHT=150
per scrollToOffset(idx*150) in die Naehe springen. FlatList rendert
die Bubbles in der Naehe, dann praezise nachsetzen nach 80ms.
2) initialNumToRender=30 (Default 10) — mehr Items beim Mount gemessen.
3) windowSize=41 (Default 21) — mehr Items im Speicher gehalten, weniger
Layout-Holes beim Weit-Scroll.
Kosten: minimal hoehere Mount-Zeit. Bei 300+ Bubbles im Backup macht
sich der UX-Gewinn lohnt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: Suche zeigt Treffer, springt aber permanent zwischen Bubbles
hin und her in Endlosschleife.
Zwei Ursachen, beide angeschlossen:
1) agent_activity-Handler rief setMessages mit prev.map() — auch wenn
keine sending-Bubble da war. Das erzeugte trotzdem ein neues Array
bei jedem Tool-Event (5-10x pro Brain-Call). invertedMessages neu →
FlatList-Layouts invalidiert mitten in einer aktiven Scroll-Sequenz.
Fix: prev.some() vor map() — wenn nichts zu aendern ist, prev
unveraendert returnen (reference-stable, kein Re-Render).
2) onScrollToIndexFailed retried unbegrenzt. Jeder failed Retry rief
den Handler erneut auf → neuer setTimeout → neuer Versuch → fail →
loop. Vorher waren cascading 3 Retries, dann auf 1 reduziert um
den 3-9-27-Cascade zu fixen, aber EIN ungebremster Retry-Schluss
pro fail bleibt eine Endlos-Schleife wenn Layouts nie stabil
werden. Fix: harter Counter (MAX_SCROLL_RETRIES = 3). Counter wird
bei jedem neuen Search-Hit via clearPendingScrollRetry resettet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der innere TouchableOpacity (eigentlich nur da um Tap-Propagation an
das aeussere close-on-tap-outside-Wrapper zu blocken) hat alle Touch-
Events konsumiert — FlatList bekam nichts ab, kein Scroll moeglich.
Fix: inner durch View ersetzen, mit onStartShouldSetResponder=true
plus onResponderTerminationRequest=false. Das blockt die Propagation
ohne Scrolls der Children zu verschlucken.
Close-on-Tap-outside funktioniert weiter (aeusseres TouchableOpacity
bleibt), das X im Header schliesst auch, Hardware-Back ebenfalls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>