feat(brain+ui+app): Triggers — passive Aufweck-Quellen fuer ARIA

ARIA hatte bisher nur ein "User fragt → Brain antwortet"-Modell. Neu:
Trigger laufen passiv im Hintergrund (kein LLM-Call) und wecken ARIA
nur dann auf wenn ein Event tatsaechlich passiert.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 00:38:58 +02:00
parent 87cb687610
commit 31aa86a2a9
10 changed files with 1190 additions and 3 deletions
+51
View File
@@ -79,6 +79,14 @@ interface ChatMessage {
active: boolean;
setupError?: string;
};
/** Trigger-Created-Bubble: ARIA hat einen neuen Trigger angelegt */
triggerCreated?: {
name: string;
type: 'timer' | 'watcher' | string;
message: string;
fires_at?: string;
condition?: string;
};
}
// --- Konstanten ---
@@ -442,6 +450,7 @@ const ChatScreen: React.FC = () => {
// gesetzt UND text leer/Placeholder)
const localOnly = prev.filter(m =>
m.skillCreated ||
m.triggerCreated ||
(m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...'))
);
// Server-Stand + lokal-only (chronologisch sortiert)
@@ -476,6 +485,26 @@ const ChatScreen: React.FC = () => {
return;
}
// trigger_created: ARIA hat einen Trigger angelegt → eigene Bubble
if (message.type === 'trigger_created') {
const p = (message.payload || {}) as any;
const triggerMsg: ChatMessage = {
id: nextId(),
sender: 'aria',
text: '',
timestamp: Date.now(),
triggerCreated: {
name: String(p.name || '(unbenannt)'),
type: String(p.type || 'timer'),
message: String(p.message || ''),
fires_at: p.fires_at ? String(p.fires_at) : undefined,
condition: p.condition ? String(p.condition) : undefined,
},
};
setMessages(prev => capMessages([...prev, triggerMsg]));
return;
}
// file_deleted: Datei wurde geloescht (vom Diagnostic User) → Bubble updaten
if (message.type === 'file_deleted') {
const p = (message.payload?.path as string) || '';
@@ -1207,6 +1236,28 @@ const ChatScreen: React.FC = () => {
? { borderWidth: 2, borderColor: '#FFD60A' }
: null;
// Spezial-Bubble: ARIA hat einen Trigger angelegt
if (item.triggerCreated) {
const t = item.triggerCreated;
const detailLine = t.type === 'timer'
? `feuert: ${t.fires_at || '?'}`
: `wenn: ${t.condition || '?'}`;
return (
<View style={[styles.messageBubble, styles.ariaBubble, {borderLeftWidth: 3, borderLeftColor: '#FFD60A'}, searchHighlightStyle]}>
<Text style={{color: '#FFD60A', fontWeight: 'bold', fontSize: 14}}>
{'⏰ ARIA hat einen Trigger angelegt'}
</Text>
<Text style={{color: '#E0E0F0', marginTop: 4, fontSize: 14}}>
<Text style={{fontWeight: 'bold'}}>{t.name}</Text>
<Text style={{color: '#8888AA', fontSize: 12}}>{` (${t.type})`}</Text>
</Text>
<Text style={{color: '#8888AA', fontSize: 12, marginTop: 2, fontFamily: 'monospace'}}>{detailLine}</Text>
<Text style={{color: '#888', fontSize: 12, marginTop: 2}}>{`"${t.message}"`}</Text>
<Text style={{color: '#555570', fontSize: 10, marginTop: 6}}>ARIA-Trigger · {time}</Text>
</View>
);
}
// Spezial-Bubble: ARIA hat einen Skill erstellt
if (item.skillCreated) {
const s = item.skillCreated;