diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx
index 86c9d47..edc882a 100644
--- a/android/src/screens/ChatScreen.tsx
+++ b/android/src/screens/ChatScreen.tsx
@@ -87,6 +87,14 @@ interface ChatMessage {
fires_at?: string;
condition?: string;
};
+ /** Memory-Saved-Bubble: ARIA hat etwas via memory_save in die Qdrant-DB gepackt */
+ memorySaved?: {
+ title: string;
+ type: string;
+ category?: string;
+ pinned: boolean;
+ preview?: string;
+ };
/** Backup-Timestamp aus chat_backup.jsonl auf dem Bridge โ Voraussetzung
* zum Loeschen der Bubble via Muelltonne. Lokale Bubbles ohne backupTs
* sind noch nicht persistiert (kurzer Race) โ Muelltonne erscheint erst
@@ -467,6 +475,7 @@ const ChatScreen: React.FC = () => {
const localOnly = prev.filter(m =>
m.skillCreated ||
m.triggerCreated ||
+ m.memorySaved ||
(m.audioRequestId && (!m.text || m.text === '๐ Aufnahme...' || m.text === 'Aufnahme...'))
);
// Server-Stand + lokal-only (chronologisch sortiert)
@@ -521,6 +530,27 @@ const ChatScreen: React.FC = () => {
return;
}
+ // memory_saved: ARIA hat etwas via memory_save Tool in die Qdrant-DB
+ // gepackt โ eigene Bubble (gelb wie trigger/skill).
+ if (message.type === 'memory_saved') {
+ const p = (message.payload || {}) as any;
+ const memoryMsg: ChatMessage = {
+ id: nextId(),
+ sender: 'aria',
+ text: '',
+ timestamp: Date.now(),
+ memorySaved: {
+ title: String(p.title || '(ohne Titel)'),
+ type: String(p.type || 'fact'),
+ category: p.category ? String(p.category) : undefined,
+ pinned: !!p.pinned,
+ preview: p.content_preview ? String(p.content_preview) : undefined,
+ },
+ };
+ setMessages(prev => capMessages([...prev, memoryMsg]));
+ return;
+ }
+
// file_deleted: Datei wurde geloescht (vom Diagnostic User) โ Bubble updaten
if (message.type === 'file_deleted') {
const p = (message.payload?.path as string) || '';
@@ -1253,6 +1283,27 @@ const ChatScreen: React.FC = () => {
? { borderWidth: 2, borderColor: '#FFD60A' }
: null;
+ // Spezial-Bubble: ARIA hat etwas via memory_save gespeichert
+ if (item.memorySaved) {
+ const m = item.memorySaved;
+ const catPart = m.category ? ` ยท [${m.category}]` : '';
+ return (
+
+
+ {'๐ง ARIA hat etwas gemerkt'}
+
+
+ {m.title}
+ {` (${m.type}${m.pinned ? ' ยท ๐ pinned' : ''}${catPart})`}
+
+ {m.preview ? (
+ {m.preview}{m.preview.length >= 140 ? 'โฆ' : ''}
+ ) : null}
+ ARIA-Memory ยท {time}
+
+ );
+ }
+
// Spezial-Bubble: ARIA hat einen Trigger angelegt
if (item.triggerCreated) {
const t = item.triggerCreated;
diff --git a/aria-brain/agent.py b/aria-brain/agent.py
index 45f3232..691cd97 100644
--- a/aria-brain/agent.py
+++ b/aria-brain/agent.py
@@ -206,6 +206,44 @@ META_TOOLS = [
},
},
},
+ {
+ "type": "function",
+ "function": {
+ "name": "memory_save",
+ "description": (
+ "Speichere eine Information dauerhaft in deinem Gedaechtnis (Qdrant-DB). "
+ "Nutze das wenn Stefan 'merk dir das' sagt oder du selbst etwas Wichtiges "
+ "festhalten willst. ALTERNATIVEN VERMEIDEN: du hast KEIN persistentes "
+ "File-Memory mehr โ schreibe nicht in `~/.claude/projects/...`, das ist tot.\n\n"
+ "Type-Wahl:\n"
+ "- identity: ARIAs Selbstbild / Wesensart (PINNED)\n"
+ "- rule: harte Regel / Sicherheit / Werte (PINNED)\n"
+ "- preference: Stefans Vorlieben/Arbeitsweise (PINNED)\n"
+ "- tool: Tool-Freigaben / Infrastruktur (PINNED)\n"
+ "- skill: Faehigkeit / Workflow-Anleitung (PINNED)\n"
+ "- fact: Wissen ueber Stefan/Welt/Sachen โ z.B. 'Stefan hat eine Cessna'. "
+ "Cold Memory, kommt nur via Semantic Search rein. **Default fuer 'merk-dir-das'-Anfragen.**\n"
+ "- reminder: Termin/Aufgabe. Fuer ARIA-soll-ausloesen lieber trigger_timer.\n\n"
+ "Wenn unsicher: type=fact, pinned=false."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "title": {"type": "string", "description": "Kurzer Titel (max ~80 Zeichen)"},
+ "content": {"type": "string", "description": "Der eigentliche Inhalt โ wird embedded fuer Semantic Search"},
+ "type": {
+ "type": "string",
+ "enum": ["identity", "rule", "preference", "tool", "skill", "fact", "conversation", "reminder"],
+ "description": "Memory-Typ (siehe oben)",
+ },
+ "category": {"type": "string", "description": "Optional, freier Tag z.B. 'meine-sachen', 'kunden', 'persoenlichkeit'"},
+ "tags": {"type": "array", "items": {"type": "string"}, "description": "Optionale Tags"},
+ "pinned": {"type": "boolean", "description": "Default false. Nur true wenn die Info IMMER im System-Prompt liegen muss (Identitaet/Regeln/Praeferenzen)."},
+ },
+ "required": ["title", "content", "type"],
+ },
+ },
+ },
]
@@ -467,6 +505,42 @@ class Agent:
else:
lines.append(f"- {t['name']} ({t['type']}, {state})")
return "\n".join(lines)
+ if name == "memory_save":
+ title = (arguments.get("title") or "").strip()
+ content = (arguments.get("content") or "").strip()
+ mem_type = (arguments.get("type") or "fact").strip()
+ if not title or not content:
+ return "FEHLER: title und content sind Pflicht."
+ valid_types = {"identity", "rule", "preference", "tool",
+ "skill", "fact", "conversation", "reminder"}
+ if mem_type not in valid_types:
+ return f"FEHLER: type muss einer von {sorted(valid_types)} sein."
+ category = (arguments.get("category") or "").strip()
+ tags_in = arguments.get("tags") or []
+ tags = [str(t).strip() for t in tags_in if str(t).strip()] if isinstance(tags_in, list) else []
+ pinned = bool(arguments.get("pinned", False))
+ try:
+ from memory import MemoryPoint
+ vec = self.embedder.embed(content)
+ point = MemoryPoint(
+ id="", type=mem_type, title=title, content=content,
+ pinned=pinned, category=category, source="aria", tags=tags,
+ )
+ pid = self.store.upsert(point, vec)
+ saved = self.store.get(pid)
+ self._pending_events.append({
+ "type": "memory_saved",
+ "memory": {
+ "id": saved.id, "type": saved.type, "title": saved.title,
+ "content_preview": (saved.content or "")[:140],
+ "category": saved.category, "pinned": saved.pinned,
+ },
+ })
+ return (f"OK โ Memory '{title}' gespeichert "
+ f"(type={mem_type}, pinned={pinned}, id={saved.id[:8]}).")
+ except Exception as e:
+ logger.exception("memory_save fehlgeschlagen")
+ return f"FEHLER beim Speichern: {e}"
return f"Unbekanntes Tool: {name}"
except Exception as exc:
logger.exception("Tool '%s' fehlgeschlagen", name)
diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py
index a81c99a..0e464e9 100644
--- a/bridge/aria_bridge.py
+++ b/bridge/aria_bridge.py
@@ -1376,6 +1376,17 @@ class ARIABridge:
})
logger.info("[brain] location_tracking Request: on=%s (%s)",
event.get("on"), event.get("reason", ""))
+ elif etype == "memory_saved":
+ # ARIA hat selber etwas in die Vector-DB gespeichert.
+ # Eigene Bubble in App + Diagnostic (gelb wie skill/trigger).
+ await self._send_to_rvs({
+ "type": "memory_saved",
+ "payload": event.get("memory", {}),
+ "timestamp": int(asyncio.get_event_loop().time() * 1000),
+ })
+ logger.info("[brain] ARIA hat eine Memory angelegt: %s (type=%s)",
+ event.get("memory", {}).get("title"),
+ event.get("memory", {}).get("type"))
# _process_core_response uebernimmt alles weitere:
# File-Marker extrahieren + broadcasten, NO_REPLY-Check, Chat-
@@ -2635,6 +2646,12 @@ class ARIABridge:
},
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
+ elif etype == "memory_saved":
+ await self._send_to_rvs({
+ "type": "memory_saved",
+ "payload": event.get("memory", {}),
+ "timestamp": int(asyncio.get_event_loop().time() * 1000),
+ })
except Exception:
logger.exception("[trigger-fire] Side-Channel-Event %s fehlgeschlagen", etype)
diff --git a/diagnostic/index.html b/diagnostic/index.html
index 7809299..5707274 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -1383,6 +1383,14 @@
}
return;
}
+ if (msg.type === 'memory_saved') {
+ addMemorySavedBubble(msg.payload || {});
+ // Falls Gehirn-Tab offen: refreshen
+ if (document.getElementById('tab-brain') && document.getElementById('tab-brain').classList.contains('visible')) {
+ loadBrainMemoryList();
+ }
+ return;
+ }
if (msg.type === 'chat_delta') { return; }
if (msg.type === 'chat_error') {
addChat('error', msg.error, 'chat:error');
@@ -1976,6 +1984,39 @@
}
}
+ /** ARIA hat eine Memory in die Qdrant-DB gespeichert โ als Bubble anzeigen. */
+ function addMemorySavedBubble(memory) {
+ const title = memory.title || '(ohne Titel)';
+ const type = memory.type || 'fact';
+ const cat = memory.category || '';
+ const pinned = !!memory.pinned;
+ const preview = memory.content_preview || '';
+ const typeLabel = (typeof BRAIN_TYPE_LABELS !== 'undefined' && BRAIN_TYPE_LABELS[type]) || type;
+ const pinBadge = pinned ? '๐ pinned' : '';
+ const catBadge = cat ? ` [${escapeHtml(cat)}]` : '';
+ const html = `
+
๐ง ARIA hat etwas gemerkt
+
+ ${escapeHtml(title)}
+ (${escapeHtml(typeLabel)})
+ ${pinBadge}${catBadge}
+
+ ${preview ? `${escapeHtml(preview)}${preview.length >= 140 ? 'โฆ' : ''}
` : ''}
+ `;
+ for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
+ if (!box) continue;
+ const el = document.createElement('div');
+ el.className = 'chat-msg received';
+ el.style.borderLeft = '3px solid #FFD60A';
+ el.innerHTML = html;
+ box.appendChild(el);
+ box.scrollTop = box.scrollHeight;
+ }
+ }
+
/** Wenn der Server file_deleted broadcastet: alle Bubbles mit
diesem serverPath rerendern als "geloescht" markieren. */
function markFileDeletedInChat(serverPath) {
diff --git a/diagnostic/server.js b/diagnostic/server.js
index afbcc51..ff15fcb 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -617,6 +617,11 @@ function connectRVS(forcePlain) {
// Mode-Broadcast von der Bridge โ an Browser-Clients weiterreichen
log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`);
broadcast({ type: "mode", payload: msg.payload });
+ } else if (msg.type === "memory_saved") {
+ // ARIA hat selber etwas in die Qdrant-DB gespeichert (via memory_save Tool).
+ const m = msg.payload || {};
+ log("info", "rvs", `ARIA-Memory gespeichert: "${m.title}" (type=${m.type}, pinned=${m.pinned})`);
+ broadcast({ type: "memory_saved", payload: m });
} else if (msg.type === "chat_message_deleted") {
// Bridge meldet: Bubble wurde aus chat_backup + Brain entfernt.
// An Browser-Clients weiterreichen damit sie die Bubble lokal entfernen.
diff --git a/rvs/server.js b/rvs/server.js
index 209bbb6..acab02c 100644
--- a/rvs/server.js
+++ b/rvs/server.js
@@ -26,6 +26,7 @@ const ALLOWED_TYPES = new Set([
"xtts_import_voice", "xtts_voice_imported",
"skill_created",
"trigger_created",
+ "memory_saved",
"location_update", "location_tracking",
"chat_history_request", "chat_history_response", "chat_cleared",
"delete_message_request", "chat_message_deleted",