Einmal-Tool zum Entfernen der aus-der-Rolle-Antworten ("das ist injiziert,
ich bin Claude Code"), die waehrend der --append-system-prompt-Phase in die
History geschrieben wurden und das Modell per Self-Grounding rueckwaerts aus
der Rolle zogen (siehe 2284c1a). Feld-agnostisch: bedient conversation.jsonl
(content/project_id) UND chat_backup.jsonl (text/projectId). Entfernt nur
Hauptthread-Assistant-Turns mit "claude code" + zweitem Ablehnungs-Marker
plus die ausloesende Frage; projekt-getaggte Turns (z.B. legitime Pentest-
Doku, die Injection als Arbeitsmaterial erwaehnt) bleiben unangetastet.
Dry-Run per Default, Backup vor --apply, idempotent.
Bereits auf der Dev-VM angewandt: je 5 Fehl-Dialoge aus beiden Dateien
entfernt, Brain neu gestartet, Hauptchat verifiziert (ARIA antwortet wieder
als ARIA mit vollem Memory-Zugriff).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Self-Grounding-Ansatz (268636c) allein reichte nicht: Sonnet 5 in Claude
Code haelt seine eingebaute "You are Claude Code"-Identitaet hart und liest den
synthetischen <previous_response> als "fabricated". Ursache bleibt der Kanal:
--append-system-prompt HAENGT ARIAs Persona nur hinten an Claude Codes Basis-
Identitaet an — bei duennem Kontext (Hauptchat) gewinnt die Basis und wehrt die
Persona als Injection ab. Anhaengen ist gegen das Anti-Injection-Training des
Modells nicht durchsetzbar.
Fix: System-Prompt VOLL ersetzen statt anhaengen — sed-Patch in docker-compose
schaltet manager.js buildArgs von --append-system-prompt auf --system-prompt.
Damit ist die ARIA-Persona DIE Identitaet des Modells (kein Anhaengsel), und
Claude Codes dynamische Sektionen (cwd '/', git, platform) — die "ich bin ein
Coding-Agent"-Signale — fallen weg. Bestaetigt per claude-code-guide: die CLI
isoliert bei --system-prompt vollstaendig, die eingebaute Identitaet leakt nicht.
extractSystemPrompt liefert weiterhin einen selbsttragenden Prompt (Persona +
Anker + Memory + Skills + Tool-Block); er darf bei --system-prompt nie leer sein
(Brain schickt immer System-Message + Tools -> real nie leer). Der IDENTITY_SEED
aus 268636c bleibt als Defense-in-Depth drin. Kommentare in openai-to-cli.js,
routes.js, agent.py, prompts.py entsprechend nachgezogen.
Deploy: Proxy UND Brain neu (--force-recreate; routes.js/openai-to-cli.js werden
frisch in den Proxy-Container ge-cp't). Verifikation am Container:
docker exec aria-proxy sh -c 'grep -o "\-\-system-prompt" /usr/local/lib/*/claude-max-api-proxy/dist/subprocess/manager.js'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Symptom: Im Hauptchat antwortete statt ARIA der Basis-Proxy-Claude ("I'm
Claude Code (Sonnet 5), I'm not adopting that persona") und flaggte GPS +
Tool-XML als Prompt-Injection. Im Projekt lief alles normal.
Ursache: Die Persona geht ueber --append-system-prompt zu — das HAENGT sie
nur hinten an Claude Codes eigene "You are Claude Code"-Basis-Identitaet an,
ersetzt sie nicht. Ob ARIA in der Rolle bleibt, entscheidet dann der
Konversations-Strom (stdin): reiche Projekt-Historie voller ARIA-
<previous_response>-Turns haelt die Rolle; der duenne Hauptchat-Verlauf
(bzw. der erste Turn eines neuen Projekts) nicht -> Basis-Identitaet gewinnt.
Der IDENTITY_ANCHOR (a0e8c23) ist inert, weil er im *angehaengten* Teil steht.
Fix: synthetischen ersten ARIA-Turn (IDENTITY_SEED) in ihrer eigenen Stimme
an den Anfang des Konversations-Stroms setzen. Das Modell setzt seine EIGENE
etablierte Stimme fort (Self-Grounding) — und weil es ein <previous_response>
ist und kein <system>-Tag, ist es kein Injection-Trigger. Rein ephemer, wird
nie in die Conversation persistiert. Gilt fuer alle Turns, deckt damit auch
die erste Frage eines frischen Projekts ab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
handleModels liest die kuratierte Tier-Liste jetzt pro Request aus
/shared/config/models.json statt sie hart zu codieren. Neue Tier-Namen oder
angepasste Beschreibungen sind damit eine reine Datei-Aenderung — kein
Code-Edit, kein Proxy-Neubau, kein Neustart (Datei bearbeiten → im Diagnostic
„↻ Aktualisieren").
- Fehlt/kaputt die Datei: eingebaute Defaults greifen und werden einmalig als
models.json angelegt, damit es was zu editieren gibt.
- ids bleiben MODEL_MAP-kompatibel (extractModel), Validierung: nicht-leeres
Array mit String-id, sonst Fallback auf Defaults.
- UI-Hinweis auf den Dateipfad ergaenzt.
- Load/Seed-Logik simuliert verifiziert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ARIA laeuft ueber das Claude-Max-Abo via CLI — waehlbar ist der Tier, nicht
eine feste Modellversion (die CLI nimmt automatisch das aktuelle Modell des
Tiers). Kein API-Key → keine echte Anthropic-Models-API; daher kuratierte Liste.
- proxy routes.js handleModels: kuratierte /v1/models mit tier/display_name/
description (ids bleiben MODEL_MAP-kompatibel: claude-sonnet-4/opus-4/haiku-4).
- diagnostic server.js: neuer GET /api/models-list — holt die Liste vom Proxy
(Frontend erreicht den Proxy nicht direkt).
- Frontend: Dropdown statt Freitext + „↻ Aktualisieren"-Button; markiert das
aktive brainModel (get_model), zeigt Beschreibung, „Setzen" schreibt brainModel
und weist auf Brain-Neustart hin. Freitext-Override bleibt unter „Erweitert".
Liste wird bei WS-Connect automatisch geladen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der Changelog hing bei 0.0.0.5 (2026-03), Projekt lief bis 0.2.0.2 (2026-07).
Nachgetragen: [Unreleased] (Identitaets-Anker, Proxy-System-Prompt, kontext-
getaggte Activity + kontext-scoped Cancel, Diagnostic Datei-Zuordnung) und der
zusammenhaengende Projekte-/Multi-Threading-Epos (0.1.9.7–0.2.0.2). Luecke
0.0.0.6–0.1.9.6 bewusst nicht rueckwirkend nacherfasst (Hinweis im Header).
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Statt System/Persona als <system>-getaggten User-Content in den Prompt zu
falten (was das Modell als Prompt-Injection fehldeutete und ARIA aus der Rolle
warf), geht sie jetzt ueber den ECHTEN System-Prompt-Kanal der Claude-CLI.
- openai-to-cli.js: openaiToCli liefert prompt = NUR Verlauf (conversationToPrompt),
systemPrompt = Persona + Tool-Block (extractSystemPrompt, ohne <system>-Tags).
- docker-compose: neue sed-Zeile schleust "--append-system-prompt",options.systemPrompt
ins buildArgs-Array von manager.js (gleicher Stil wie die bestehenden
Patches; options ist dort in scope). routes.js reicht systemPrompt bereits
an subprocess.start durch (b9150f2).
systemPrompt ist immer ein String (extractSystemPrompt → "" statt undefined),
also kein spawn-Crash bei leerem System. sed-Transformation gegen echten
buildArgs simuliert → valides JS-Array verifiziert.
Rollback falls die CLI-Version --append-system-prompt nicht kennt: die eine
sed-Zeile aus docker-compose entfernen + openaiToCli.prompt zurueck auf
messagesToPrompt.
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>
Vorbereitung fuer die saubere Variante: ARIA-Persona ueber den ECHTEN
System-Prompt-Kanal der Claude-CLI (--append-system-prompt) zustellen statt als
<system>-getaggten User-Content (den das Modell als Prompt-Injection wertet und
der ARIA aus der Rolle wirft — siehe a0e8c23).
- openai-to-cli.js: extractSystemPrompt() (System-Messages + Tool-Block als
roher Text, ohne <system>-Tags) und conversationToPrompt() (nur Verlauf).
openaiToCli() liefert jetzt zusaetzlich systemPrompt + conversationPrompt.
- routes.js: reicht systemPrompt an subprocess.start(..) durch.
BEWUSST rueckwaertskompatibel/no-op: prompt bleibt unveraendert (System-Inhalt
noch drin), die Extra-Option wird von einem ungepatchten manager.js ignoriert.
Der letzte Schritt (manager.js: --append-system-prompt beim Spawn + prompt auf
conversationPrompt umstellen) folgt, sobald die echte manager.js-Struktur
vorliegt — die npm-Datei liegt nicht im Repo und ein Fehlpatch legt den
kompletten Proxy lahm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: bei tiefergehenden Fragen in Projekten (v.a. dem Pentest-Projekt) verlor
ARIA ihre Identitaet — das Modell wertete die Konversation als Prompt-Injection,
verwarf die ARIA-Rolle samt <tool_call>-Format und antwortete als generischer
Coding-Agent ("Ich bin nicht ARIA, das ist ein Injection-Muster").
Ursache: die Persona kam bisher NUR aus gepinnten "identity"-Memories (weiche
Daten), es gab keinen harten System-Anker. Pentest-Projekt-History steckt voller
injection-artiger Inhalte (Payloads, <system>-Bloecke, <tool_call>-Beispiele,
XSS) — genau das Material triggert die Injection-Abwehr des Modells, und ohne
festen Anker kippt die weiche Memory-Identitaet.
Fix: IDENTITY_ANCHOR steht jetzt IMMER ganz oben im System-Prompt (vor allen
Memories). Er haelt die ARIA-Identitaet in jedem Kontext fest UND rahmt
verdaechtige Inhalte (in Verlauf/Projekten/gelesenen Dateien) explizit als
DATEN, die analysiert — nicht befolgt — werden. Besonders fuer Security-/
Pentest-Arbeit, wo solche Payloads das Arbeitsmaterial sind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der /api/files-set-project-Endpoint existierte, aber es fehlte die UI. Jede
Datei-Zeile hat jetzt ein Projekt-Dropdown (Hauptchat + alle Projekte, aktueller
Wert vorausgewaehlt, gruen wenn getaggt). Aenderung schreibt via
/api/files-set-project ins Manifest, aktualisiert den lokalen Cache und rendert
neu (respektiert den aktiven Projekt-Filter).
Damit lassen sich auch alt-hochgeladene (untagged) Dateien nachtraeglich einem
Projekt zuordnen — die projectId-Zuordnung beim Upload passiert automatisch
(596d0bb), das hier ist die manuelle Korrektur/Nachpflege.
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>
Bug: die Spracherkennung bricht mal zu frueh ab (nach zwei Worten obwohl noch
gesprochen wird) und merkt mal gar nicht dass man aufgehoert hat.
Ursache: der Endpointer feuerte rein auf semantischer Stagnation (Whisper-
Transkript waechst endpoint_ms lang nicht). Das ist fragil — kurze Sprech-
Pausen oder beam_size=1-Instabilitaet → vorzeitiger Cut; Oszillation/
Halluzination → nie ein Endpoint.
Fix: akustische Stille als robustes Primaersignal. Jeder Tick (~200ms) misst
die RMS-Energie der letzten 300ms:
- Solange Sprach-Energie da ist, bleibt die Session am Leben (kein Cut mitten
im Reden).
- Endpoint feuert wenn seit endpoint_ms keine Sprach-Energie mehr da ist.
- Semantischer Backstop (2x endpoint_ms) fuer laute Umgebungen (Auto), wo die
Energie nie faellt — dort degradiert es sauber aufs bisherige Verhalten.
RMS-Threshold (0.012) gegen Stille/Sprache/Fahrgeraeusch verifiziert.
Konstanten oben in der Datei tunebar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: bei manuellem Stop landete die Sprachnachricht (und ARIAs Antwort) im
Hauptchat statt im fokussierten Projekt — mal so, mal so, je nachdem ob der
Endpoint natuerlich kam oder man den Stop-Button drueckte.
Ursache: stt_stream_end popte die Focus-projectId-Registry SOFORT. Nach einem
manuellen Stop folgt aber noch der finale stt_endpoint (Whisper-Final-
Transcribe), der die projectId aus genau dieser Registry braucht. War sie schon
weg → focused_pid="" → Voice-Router faellt auf Hauptchat zurueck. Die
User-Bubble (lokal getaggt) blieb im Projekt, ARIAs Antwort kam aber mit
project_id="" → nur in EINEM Kontext sichtbar.
Fix: stt_stream_end raeumt die Registry nicht mehr sofort auf, sondern per
verzoegertem call_later(20s) als Leak-Schutz. Der stt_endpoint-Handler popt
selbst wenn er zuerst kommt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression aus dem chat_history-projectId-Fix: nach dem History-Reload laeuft
jetzt updateChatVisibilityByFocus(). addAriaFile() setzte aber kein
dataset.projectId → Datei-/Bild-Bubbles galten als Hauptchat und wurden
ausgeblendet, sobald der (aus localStorage wiederhergestellte) Focus auf einem
Projekt lag. In der App trat das nicht auf (datengetriebener Filter mit
korrektem projectId).
- addAriaFile(): setzt dataset.projectId aus p.projectId und respektiert beim
Live-Anhaengen den aktuellen Focus (wie addChat via hiddenByFocus).
- chat_history-Renderer: reicht m.projectId an addAriaFile weiter.
- Bridge _process_core_response: haengt turn_pid als f["projectId"] an die
file_from_aria-Payload, damit der Live-Pfad die Zuordnung kennt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1 matchte text==content exakt und verfehlte damit Nachrichten, bei denen die
Bridge den Brain-Text anreichert oder cleant:
- User-Turns: _build_core_text PREPENDT [Hinweis: Barge-In]-/[GPS-Position]-
Bloecke — die stehen in conversation.jsonl, nicht in chat_backup.
- Assistant-Turns: conversation enthaelt [FILE: /shared/uploads/...]-Marker,
chat_backup hat sie schon rausgecleant.
_norm entfernt jetzt FILE-Marker + fuehrende [..]-Bloecke, kollabiert
Whitespace und vergleicht einen 120-Zeichen-Praefix. Neuer Marker (v2) →
laeuft einmal neu und fuellt die von v1 verbliebenen Luecken. Nicht-destruktiv
(eigene .pre-backfill-v2.bak), bestehende Tags bleiben unangetastet.
Mit Tests gegen GPS-/Barge-In-/FILE-Marker-Faelle verifiziert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der Reload-Pfad des Dashboards warf die Kontext-Zuordnung an ZWEI Stellen weg,
weshalb nach jedem Reload alles im Hauptchat lag (Projekte leer) — unabhaengig
davon ob chat_backup korrekt getaggt war:
- server.js (History-Builder): pushte {type,text,meta,ts} ohne project_id.
Jetzt projectId aus obj.project_id fuer sent/received/aria_file mitgegeben.
- index.html (chat_history-Renderer): setzte dataset.ts aber nie
dataset.projectId → jede Bubble galt als Hauptchat. Jetzt gesetzt, und nach
dem Neuaufbau updateChatVisibilityByFocus() aufgerufen damit der aktuelle
Kontext-Focus sofort greift.
Zusammen mit der Backfill-Migration (8b567e1) erscheinen alt-getaggte Turns
jetzt beim Dashboard-Reload im richtigen Projekt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Projekt-Nachrichten aus der Zeit vor dem chat_backup-project_id-Feld (getaggt in
conversation.jsonl seit fc0f91d, aber chat_backup fuehrte project_id erst ab
f51ad15) lagen in der UI im Hauptchat statt im Projekt. Die App/Diagnostic
zeigen aus chat_backup.jsonl — dort fehlte der Tag.
Neue Einmal-Migration (Brain-Lifespan) schreibt project_id aus conversation.jsonl
per (role, text)-Match reihenfolge-erhaltend nach chat_backup.jsonl zurueck:
- idempotent via Marker /shared/config/.chat_backup_projectid_backfill_v1
- nicht-destruktiv: legt .pre-backfill-v1.bak an, setzt nur LEERE project_ids,
entfernt/aendert nie einen bestehenden Tag
- atomar (tmp + os.replace)
- Duplikate: Deque je (role, normtext) inkl. "" fuer Hauptthread → korrekte
Zuordnung auch bei wiederholten Texten, kein faelschliches Taggen von
Hauptchat-Interleaving
Mit Logik-Tests (Zuordnung, Duplikat-Reihenfolge, Idempotenz) verifiziert.
Nachrichten aus der Zeit bevor es Projekte gab bleiben untagged im Hauptchat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Das Dashboard hatte dieselbe Ursache wie die App (untagged Nachrichten in
chat_backup) PLUS zwei eigene untagged-Pfade ueber den Gateway-Watch:
- Frontend: chat_final rendert keine Chat-Bubble mehr. ARIA-Antworten kommen
ausschliesslich via rvs_chat (traegt projectId). chat_final stammt vom
Gateway-Watch ohne projectId → erzeugte eine Duplikat-Bubble im Hauptchat.
- server.js: kein chat_backup-Write im Gateway-final-Handler mehr. Die Bridge
(_process_core_response) ist massgeblicher Writer und schreibt MIT project_id;
der Write hier erzeugte untagged Duplikate, die beim Reload im Hauptchat
statt im Projekt landeten.
Der App-Focus-Fix (63dde65) sorgt dafuer dass neue Nachrichten ueberhaupt
getaggt in chat_backup landen — davon profitiert das Dashboard direkt.
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 4 vom Multi-Threading-Redesign — der Voice-Layer routet STT-Text
per-Projekt und lässt Meta-Kommandos gar nicht erst ans Brain.
Voice-Router in _process_endpoint_text():
- „zurueck zum hauptchat" / „hauptchat bitte" / „aria hauptchat"
→ Sticky reset, project_changed(exited) broadcasten, KEIN Brain-Call.
- „fuer <name>: <text>" (Fuzzy-Match auf Projekt-Namen ≥ 0.6 Score)
→ Sticky auf gefundene project_id + Rest des Texts geht ans Brain
im Projekt-Kontext. project_changed(entered) broadcasten damit
App/Diagnostic den Focus mit umschalten.
- Sticky-Timeout 30s: eine Voice-Message ohne Prefix innerhalb des
Fensters bleibt im Sticky-Projekt, refresht das Timeout. Nach Ablauf
→ Default Hauptchat.
- Meta-Kommandos aendern KEINEN Brain-State — ARIAs Arbeit in laufenden
Projekten wird nicht abgebrochen.
send_to_core wird jetzt mit dem gerouteten project_id gerufen; das Brain
bekommt den Text im richtigen Queue-Kontext.
Broadcast-Chain: Voice-Router setzt Sticky → project_changed geht via RVS
an App+Diagnostic → Focus-Header/Kontext-Strip wechseln automatisch.
Damit ist der komplette Multi-Threading-Redesign abgeschlossen:
- Brain: per-Request project_id + per-Projekt Queue + Queue-Aware Prompt
- Bridge: Chat-Routing + Voice-Router
- App: Focus-One + Drawer + Status-Dots
- Diagnostic: Kontext-Strip + Focus-Filter
- Voice: Sticky + Meta-Interception
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 vom Multi-Threading-Redesign. Diagnostic zeigt einen scrollbaren
Streifen von Kontext-Karten ueber dem Chat (Hauptchat + Projekte), jede
mit Live-Status-Dot. Tap wechselt den Focus, Chat filtert auf diesen
Kontext, Sende-Input laeuft mit der Focus-ID durch Bridge → Brain-Queue.
index.html:
- Neuer <div id="chat-context-strip"> ueber der Chat-Box, horizontal
scrollbar.
- JS: focusedContextId (in localStorage gespiegelt), diagQueueStatus,
diagProjectsCache. renderContextStrip() zeichnet Karten mit Dot
+ Status-Label. switchDiagFocus(id) wechselt Focus + versteckt
Bubbles anderer Kontexte via data-project-id + style.display.
- Polling: /api/brain/projects/queue-status alle 2s, /projects/list
alle 15s.
- addChat: nimmt options.projectId → schreibt data-project-id an die
DOM-Node, versteckt sofort wenn Focus abweicht.
- Chat-Reception-Handler propagiert p.projectId aus dem RVS-Payload.
- testRVS() sendet msg.projectId=focusedContextId mit.
server.js:
- sendToRVS(text, isTrace, projectId): neuer Param, wird in
payload.projectId gesetzt → Bridge routet an /chat body.project_id.
- test_rvs-Handler reicht msg.projectId durch.
Bewusst nicht drin (Follow-up wenn Stefan mag):
- Voller Dashboard-Stack mit stacked Karten die eigene Message-Listen +
Input-Felder haben. Aktuelle Variante ist „Kontext-Strip fuer schnellen
Wechsel + Focus-One-Rendering" — ~90% des UX-Werts mit ~10% des Aufwands.
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>
Erster Schritt zum echten Multi-Threading fuer ARIA-Projekte. Kein globaler
active_project-State mehr — jeder /chat-Request sagt selbst welche Buehne
(project_id im Body). Verschiedene Projekte laufen parallel, gleiches
Projekt queued via asyncio.Lock.
Backend:
- ChatIn.project_id: Client bestimmt pro Request wohin. Bridge routet.
- /chat: async, holt per-Projekt asyncio.Lock. Requests fuers gleiche
Projekt reihen sich in _project_pending ein, warten am Lock. Requests
fuer verschiedene Projekte laufen echt parallel.
- Neuer /projects/queue-status endpoint: pro Kontext (inkl. Hauptchat
unter __main__): busy True/False + queue_size. Fuers UI-Status-Dots.
- Agent.chat() nimmt project_id + pending_queue Params. Kein
projects_mod.get_active() mehr im Hot-Path.
Queue-Aware Prompting:
- Wenn nach dem aktuellen Turn weitere Nachrichten in der Queue liegen,
wird der System-Prompt um ein QUEUE-Segment erweitert mit Instruktion:
„Bevor Du den aktuellen Task loesst, pruef die Queue — widerspricht/
annuliert eine spaetere Nachricht? Dann Skip-Antwort statt Doppelarbeit."
- Beispiel: Task 'titelleiste rot' + Queue-Tail 'doch nicht, blau'
→ ARIA skipt rot, blau kommt als naechste Anfrage sauber durch.
- Kein extra LLM-Call — reine Prompt-Injection.
Project-Tools:
- project_enter/exit sind jetzt UI-Signale (App wechselt Ansicht via
project_changed event), aendern KEINEN Brain-State mehr. Der aktuelle
Turn bleibt in seinem Chat-Kontext.
- project_list zeigt keinen "AKTIV"-Marker mehr (nicht mehr sinnvoll).
- projects_mod.set_active/get_active bleiben als Legacy-Helpers (kein
Aufruf mehr aus dem Hot-Path).
Bridge:
- send_to_core packt project_id in den /chat-Body.
- User-Backup-Eintrag tag't project_id sauber, keine Brain-Query mehr.
Naechste Schritte (kommende Commits):
- App: Focus-One-View mit Drawer + Status-Dots + OS-Push
- Diagnostic: Dashboard-Stack mit Karten
- Voice-Router: 30s-Sticky + Meta-Command-Interception im wakeword.ts
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>
Bisher haben nur App-Uploads (msg_type == "file") ein Projekt-Tag bekommen.
Dateien die ARIA waehrend des Turns selbst schreibt (via [FILE: /shared/
uploads/aria_xyz.pdf]-Marker) sind dem Hauptchat zugefallen, auch wenn
Stefan in einem Projekt war.
Fix: Beim Verarbeiten der ARIA-Antwort in _process_core_response wird die
turn_project_id aus payload.projectId (ChatOut.project_id vom Brain) ge-
nutzt um jede gefundene ARIA-Datei sofort zu taggen, bevor sie als
file_from_aria broadcast wird.
Helper-Split:
- _tag_file_to_project(path, pid): pure Write, pid schon bekannt
- _tag_file_to_active_project(path): Convenience-Wrapper, fragt Brain
nach active project (genutzt vom App-Upload-Handler, der noch nichts
vom Projekt-Kontext weiss)
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>
Sobald eine Streaming-Session ~1.5s Audio im Buffer hat, wird einmal pro
Session der Speaker-ID-Check ausgefuehrt (im Executor, ~50-100ms auf GPU).
Bei Match → Session laeuft normal weiter. Bei Mismatch → synthetisches
stt_endpoint mit text='' reason='speaker_mismatch' + stt_stream_done →
App ruft endConversation. Kein Whisper-Transcribe fuer fremde Stimmen →
Token + Latenz gespart.
- StreamSession: 3 neue Felder (speaker_checked, speaker_match,
speaker_similarity).
- SessionManager._check_speaker / _finalize_speaker_mismatch:
Check + sauberes Beenden bei Mismatch.
- _tick_session: Check-Gate vor STREAM_MIN_AUDIO_MS-Check eingehaengt.
- speaker_id.verify: threshold=None statt =DEFAULT_THRESHOLD damit
config-Broadcast-Updates zur Laufzeit greifen (Default-Arg wird sonst
zur Def-Zeit gebunden).
Fail-open: ohne Fingerprint returnt verify() (True, 0.0) — keine
Auswirkung. Stefan kann ohne Enrollment weiter wie bisher arbeiten.
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>
Bug: ARIAs VOICE_COMMUNICATION-Lock liess WhatsApp-Sprachnachrichten,
Sprach-Suchen u.ae. nur Stille aufnehmen — Android-Audio-Policy gibt
der zweiten App formal Audio, liefert aber Null-Samples solange unsere
Pipeline aktiv ist.
Fix: AudioRecordingCallback (API 24+) registriert sich beim start() und
beobachtet andere Mic-Sessions. Fremder Mic-User detected → unsere
AudioRecord + Effects sofort freigeben (externallyPaused=true), WakeLock
+ Callback bleiben aktiv. Fremder weg → 300ms warten (Audio-Stack-
Settling), nochmal pruefen, dann reaktivieren.
Refactor mit drin: start()/stop() benutzen jetzt zwei Helper
(acquireAndStartRecording, stopAndReleaseRecording) damit die Mic-Setup-
Logik nicht zwischen start() und resumeAfterExternal() dupliziert wird.
Trade-offs:
- Wake-Word taub solange andere App das Mic nutzt — akzeptabel.
- API < 24: kein Callback verfuegbar, alter Stand (kein Mic-Sharing).
- Phone-Call kollidiert nicht mit phoneCallService.pauseForCall —
beide pausieren/reaktivieren idempotent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frueher: Spotify-spezifische Patterns hardcoded in agent.py — jeder neue
Steuer-Skill haette wieder Brain-Code-Aenderungen gebraucht.
Jetzt: jeder Skill deklariert seine eigenen Patterns im Manifest unter
fast_patterns: [{match, args, reply}]. Brain iteriert generisch, kein
Skill bekommt Sonderbehandlung.
- agent.py: _try_skill_fast_path liest aus skills.list_skills(), keine
Spotify-Konstanten mehr. skill_create/skill_update Tool-Schema kennt
fast_patterns (mit Beispiel + Wann-nutzen-Hinweis).
- skills.py: _normalize_fast_patterns validiert Regex + filtert kaputte
Eintraege; create_skill/update_skill akzeptieren das Feld.
- main.py: einmalige Lifespan-Migration — wenn spotify-Skill existiert
und kein fast_patterns hat, werden die alten Hardcoded-Patterns
rueberkopiert. Idempotent, laeuft bei jedem Restart sicher mehrfach.
- seed_rules.py: neue Regel `seed/skill-rule/fast-patterns-for-control`
erklaert ARIA wann sie das Feature nutzen soll (reines Steuern: ja,
kreativer Output / Parametrisierung: nein) — mit Beispiel.
Trade-off: Volume-Patterns (lauter/leiser) fallen aus dem Fast-Path raus,
weil die Multi-Step-Logik (GET state → compute → PUT) sich nicht
deklarativ ausdruecken laesst. Wer das zurueck will: Spotify-Skill um
einen action=volume_relative-Arg erweitern der die Mathe intern macht.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>