Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28361ea97a | ||
|
|
a20e57e33a | ||
|
|
dc775ee34f | ||
|
|
c2122c43c2 |
@@ -10,6 +10,44 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
|
||||
|
||||
---
|
||||
|
||||
## [0.2.2.3] — 2026-07-17 — ARIA liest andere Projekt-Chats wirklich (volle Historie)
|
||||
|
||||
### Behoben
|
||||
|
||||
- **`project_summary` fand fast nie Inhalte.** Es las nur ARIAs rollendes Kontextfenster (~50 Turns über ALLE Projekte) — ältere/andere Projekt-Chats sind da längst rausdistilliert. „Hol dir die Infos aus Projekt X" lieferte deshalb leere Ergebnisse, und ARIA wusste nicht, wie sie an die Historie kommt. Jetzt liest das Tool die **echte, volle Historie aus `chat_backup.jsonl`** (im Brain gemountet) — die letzten ~20 Turns des Zielprojekts, unabhängig davon wie lange man da nicht war. Standort-Hints in User-Turns werden rausgefiltert. Fallback aufs Fenster, falls das Backup fehlt. Tool-Beschreibung geschärft, damit ARIA es direkt aufruft.
|
||||
- Nebenbei bereinigt: `mac_os_update_fehler` war durch den alten `set_project_kind`-Bug (0.2.2.1) fälschlich `kind=code` — auf `chat` zurückgesetzt.
|
||||
|
||||
### Deploy
|
||||
`git pull && docker compose up -d --build brain` (kein APK-Rebuild nötig).
|
||||
|
||||
---
|
||||
|
||||
## [0.2.2.2] — 2026-07-17 — Voice-Projektwechsel zurück — aber gerätelokal
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **„ARIA, geh in Projekt X" per Sprache wechselt wieder das Projekt** — aber **nur auf dem Gerät, das den Befehl gab**. Andere App-Instanzen und das Diagnostic bleiben in ihrem Projekt.
|
||||
- Umsetzung: Jedes Gerät merkt sich die IDs seiner eigenen Anfragen (Text-`clientMsgId` + Voice-`audioRequestId`). Der Brain/Voice-Router hängt an jedes `project_changed`-Event die auslösende `clientMsgId` an; die App folgt dem Wechsel nur, wenn die ID eine **eigene** ist. Deckt beide Voice-Pfade ab (ARIAs `project_enter`/`exit` via `send_to_core` **und** den Bridge-Voice-Router „für Projekt X: …" / „zurück zum Hauptchat").
|
||||
- Manuelles Umschalten bleibt wie gehabt gerätelokal über den Drawer.
|
||||
|
||||
### Deploy
|
||||
`git pull && docker compose up -d --build bridge` + APK 0.2.2.2 (kein Brain-Rebuild nötig).
|
||||
|
||||
---
|
||||
|
||||
## [0.2.2.1] — 2026-07-17 — Projekt-Fokus wirklich pro Gerät unabhängig
|
||||
|
||||
### Behoben
|
||||
|
||||
- **`set_project_kind` markierte das falsche Projekt.** Es nutzte den **globalen** `active_project` (geräteübergreifend) statt des Projekts, in dem der aktuelle Request läuft. Folge: Obwohl auf der App „Basic os" aktiv war, bekam das global-aktive „Mac OS Update Fehler" `kind=code`. Jetzt wirkt das Tool auf die pro Request mitgesendete `project_id` (in `_dispatch_tool` durchgereicht) — jede App-/Diagnostic-Instanz markiert ihr eigenes Projekt.
|
||||
- **App wurde von fremden `project_changed`-Broadcasts ins andere Projekt gezogen.** Ein Projektwechsel/‑anlegen durch ARIA, das Diagnostic oder eine andere App-Instanz erzwang auf **allen** Geräten einen Fokuswechsel. Der Projekt-Fokus ist jetzt **rein gerätelokal**: Broadcasts aktualisieren nur Namen + Typ, wechseln aber nicht mehr das Projekt. Umschalten geht ausschließlich lokal über den Projekt-Drawer. So arbeitet jedes Gerät unabhängig in seinem eigenen Projekt (auch mehrere App-Instanzen).
|
||||
- Hinweis: „ARIA, geh in Projekt X" per Sprache wechselt das App-Projekt dadurch **nicht mehr** automatisch — bewusst, damit Geräte unabhängig bleiben. Wechseln über den Drawer.
|
||||
|
||||
### Deploy
|
||||
`git pull && docker compose up -d --build brain` + APK 0.2.2.1 neu bauen.
|
||||
|
||||
---
|
||||
|
||||
## [0.2.2.0] — 2026-07-17 — Workbench: Taskleisten-Dock statt Zoom-Landkarte + bedienbare VNC-Konsole
|
||||
|
||||
### Geändert
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 20201
|
||||
versionName "0.2.2.1"
|
||||
versionCode 20202
|
||||
versionName "0.2.2.2"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.2.2.1",
|
||||
"version": "0.2.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -407,6 +407,22 @@ const ChatScreen: React.FC = () => {
|
||||
const nextClientMsgId = (): string =>
|
||||
`cmsg_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;
|
||||
|
||||
// IDs (clientMsgId + audioRequestId), die DIESES Geraet ausgeloest hat.
|
||||
// Fuer den geraete-lokalen Voice-Projektwechsel: ein project_changed vom Brain
|
||||
// traegt die ausloesende ID; nur wenn sie zu einer unserer IDs gehoert, folgt
|
||||
// dieses Geraet dem Wechsel. So wechselt „ARIA, geh in Projekt X" nur das
|
||||
// Geraet, das den Befehl gab — nicht andere App-/Diagnostic-Instanzen.
|
||||
const myRequestIdsRef = useRef<Set<string>>(new Set());
|
||||
const rememberMyRequest = (id: string): void => {
|
||||
if (!id) return;
|
||||
const s = myRequestIdsRef.current;
|
||||
s.add(id);
|
||||
if (s.size > 80) {
|
||||
const oldest = s.values().next().value;
|
||||
if (oldest !== undefined) s.delete(oldest);
|
||||
}
|
||||
};
|
||||
|
||||
// Wie lange wir auf das ACK warten bevor wir retryen. Bridge sollte
|
||||
// unmittelbar zurueckmelden — 30s ist grosszuegig fuer schlechte Netze.
|
||||
// 60s — grosszuegiger als 30s, weil langsame Brain-Calls (Multi-Tool) sonst
|
||||
@@ -462,6 +478,7 @@ const ChatScreen: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
pendingPayloads.current.set(cmid, { type, payload });
|
||||
rememberMyRequest(cmid);
|
||||
const online = connectionStateRef.current === 'connected';
|
||||
if (!online) {
|
||||
updateMessageStatus(cmid, { deliveryStatus: 'queued', sendAttempts: attempt });
|
||||
@@ -992,10 +1009,18 @@ const ChatScreen: React.FC = () => {
|
||||
if (p.id && (p.kind === 'code' || p.kind === 'chat')) {
|
||||
projectFocus.setKind(p.id, p.kind);
|
||||
}
|
||||
if (action === 'entered' || action === 'created') {
|
||||
if (p.id) setFocusedProjectId(p.id);
|
||||
} else if (action === 'exited') {
|
||||
setFocusedProjectId('');
|
||||
// Projekt-Fokus ist GERAETE-LOKAL. Ein Broadcast wechselt dieses Geraet
|
||||
// NUR, wenn er durch EINEN EIGENEN Befehl ausgeloest wurde — erkennbar an
|
||||
// der mitgelieferten clientMsgId (unsere gesendete Text-cmid oder Voice-
|
||||
// audioRequestId). So folgt „ARIA, geh in Projekt X" nur dem Geraet, das
|
||||
// den Befehl gab; Diagnostic / andere App-Instanzen bleiben unberuehrt.
|
||||
const trigCmid = typeof p.clientMsgId === 'string' ? p.clientMsgId : '';
|
||||
if (trigCmid && myRequestIdsRef.current.has(trigCmid)) {
|
||||
if (action === 'entered' || action === 'created') {
|
||||
if (p.id) setFocusedProjectId(p.id);
|
||||
} else if (action === 'exited') {
|
||||
setFocusedProjectId('');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1620,6 +1645,7 @@ const ChatScreen: React.FC = () => {
|
||||
// dem finalen Text, dann wird die Bubble ueber audioRequestId-Match
|
||||
// aktualisiert (siehe chat-Handler oben).
|
||||
const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
rememberMyRequest(audioRequestId);
|
||||
const wasInterrupted = interruptAriaIfBusy();
|
||||
const location = await getCurrentLocation();
|
||||
const windowMs = await loadConvWindowMs();
|
||||
@@ -1723,6 +1749,7 @@ const ChatScreen: React.FC = () => {
|
||||
// Kurze Pause damit halt durchgreift, dann neue Aufnahme starten
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
rememberMyRequest(audioRequestId);
|
||||
const location = await getCurrentLocation();
|
||||
const windowMs = await loadConvWindowMs();
|
||||
|
||||
@@ -1799,6 +1826,7 @@ const ChatScreen: React.FC = () => {
|
||||
// useCallback damit der useEffect oben die Funktion stabil capturen kann.
|
||||
const startPassiveStreamingRecording = useCallback(async () => {
|
||||
const audioRequestId = `audio_passive_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
rememberMyRequest(audioRequestId);
|
||||
const location = await getCurrentLocation();
|
||||
const passiveMs = await loadPassiveListenMs();
|
||||
const { ok } = await audioService.startStreamingRecording({
|
||||
@@ -2202,6 +2230,7 @@ const ChatScreen: React.FC = () => {
|
||||
// ueber audioRequestId-Match nachgereicht wenn whisper das Endpoint feuert.
|
||||
const handleVoiceButtonStart = useCallback(async (): Promise<boolean> => {
|
||||
const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
rememberMyRequest(audioRequestId);
|
||||
const wasInterrupted = interruptAriaIfBusy();
|
||||
const location = await getCurrentLocation();
|
||||
|
||||
|
||||
+79
-23
@@ -1045,12 +1045,15 @@ META_TOOLS = [
|
||||
"function": {
|
||||
"name": "project_summary",
|
||||
"description": (
|
||||
"Schau in einen ANDEREN Chat rein und fass zusammen was dort zuletzt "
|
||||
"passiert ist (letzte ~12 Turns). Funktioniert fuer jedes Projekt (per "
|
||||
"Name, Fuzzy-Match) UND fuer den Hauptchat (name='Hauptchat'). Nutze es "
|
||||
"IMMER wenn Stefan sagt 'hol dir die Infos aus Projekt X', 'schau mal in "
|
||||
"den Hauptchat/in Projekt Y rein', 'was war zuletzt bei ...', 'hol mich "
|
||||
"ab' — sonst halluzinierst Du Inhalte die nicht da sind."
|
||||
"Liest die ECHTE gespeicherte Chat-Historie eines ANDEREN Projekts "
|
||||
"(letzte ~20 Turns aus dem persistierten Verlauf — nicht nur Deinem "
|
||||
"aktuellen Kontextfenster) und fasst sie zusammen. Funktioniert fuer "
|
||||
"JEDES Projekt (per Name, Fuzzy-Match) UND fuer den Hauptchat "
|
||||
"(name='Hauptchat'). Rufe es DIREKT auf, sobald Stefan sagt 'hol dir "
|
||||
"die Infos aus Projekt X', 'schau mal in Projekt Y / den Hauptchat "
|
||||
"rein', 'was war zuletzt bei ...', 'hol mich ab' — antworte NICHT aus "
|
||||
"dem Gedaechtnis, sonst halluzinierst Du. Der Verlauf des Projekts steht "
|
||||
"sicher zur Verfuegung, auch wenn Ihr da lange nicht wart."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -1515,7 +1518,7 @@ class Agent:
|
||||
targs = {}
|
||||
logger.info("[router] lokal Tool-Call: %s(%s)", tname,
|
||||
", ".join(targs.keys()))
|
||||
tresult = self._dispatch_tool(tname, targs)
|
||||
tresult = self._dispatch_tool(tname, targs, active_project_id)
|
||||
# Echter Skill (run_*) erfolgreich? Dann uebernimmt dieser
|
||||
# Turn das speak-Flag des Skills (Steuerbefehl=stumm,
|
||||
# Antwort-Skill=vorlesen). Letzter Skill gewinnt.
|
||||
@@ -1807,7 +1810,7 @@ class Agent:
|
||||
))
|
||||
# Tools ausfuehren + Ergebnis als role=tool zurueck
|
||||
for tc in result.tool_calls:
|
||||
tool_result = self._dispatch_tool(tc["name"], tc["arguments"])
|
||||
tool_result = self._dispatch_tool(tc["name"], tc["arguments"], active_project_id)
|
||||
# Steuerbefehl-Skill (run_*, speak=false) erfolgreich
|
||||
# ausgefuehrt → Antwort nicht vorlesen (letzter Skill gewinnt).
|
||||
_tn = tc.get("name") or ""
|
||||
@@ -1895,10 +1898,52 @@ class Agent:
|
||||
|
||||
# ── Tool-Dispatcher ───────────────────────────────────────
|
||||
|
||||
def _dispatch_tool(self, name: str, arguments: dict) -> str:
|
||||
def _read_project_history(self, project_id: str, limit: int = 20) -> list:
|
||||
"""Liest die VOLLE Chat-Historie eines Projekts aus dem geteilten
|
||||
chat_backup.jsonl (/shared/config, auch im Brain gemountet).
|
||||
|
||||
Noetig, weil das rollende Conversation-Window nur die letzten ~50 Turns
|
||||
ueber ALLE Projekte haelt — aeltere/andere Projekt-Chats sind da laengst
|
||||
rausdistilliert. Gibt die letzten `limit` (role, text)-Paare zurueck,
|
||||
gefiltert auf project_id ('' = Hauptthread). Fuehrende Hint-Bloecke
|
||||
([Standort: ...]) in User-Turns werden entfernt."""
|
||||
path = "/shared/config/chat_backup.jsonl"
|
||||
out: list = []
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
o = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
role = o.get("role")
|
||||
text = o.get("text")
|
||||
if not role or text is None:
|
||||
continue
|
||||
# nur echte Gespraechs-Turns (keine skill/memory/system-Eintraege)
|
||||
if role not in ("user", "assistant", "aria"):
|
||||
continue
|
||||
if (o.get("project_id") or "") != (project_id or ""):
|
||||
continue
|
||||
if role == "user":
|
||||
text = _strip_leading_hint_blocks(text)
|
||||
out.append((role, text))
|
||||
except Exception as exc:
|
||||
logger.warning("project_summary: chat_backup lesen fehlgeschlagen: %s", exc)
|
||||
return []
|
||||
return out[-limit:]
|
||||
|
||||
def _dispatch_tool(self, name: str, arguments: dict, project_id: str = "") -> str:
|
||||
"""Fuehrt einen Tool-Call aus und gibt ein kurzes Text-Resultat zurueck.
|
||||
Niemals werfen — Fehler werden als Text-Resultat reportet damit Claude
|
||||
weitermachen kann."""
|
||||
weitermachen kann. project_id = Projekt DIESES Requests (fuer Tools die
|
||||
aufs aktuelle Projekt wirken, z.B. set_project_kind) — NICHT der globale
|
||||
active_project (geraeteuebergreifend)."""
|
||||
try:
|
||||
if name == "skill_create":
|
||||
# ARIA-Skills sind immer Python — execution ist nicht mehr im Schema
|
||||
@@ -2566,23 +2611,29 @@ class Agent:
|
||||
# aus einem Projekt heraus in den Hauptthread reinschauen kann.
|
||||
if pname.lower() in {"hauptchat", "hauptthread", "haupt", "main",
|
||||
"mainchat", "haupt-chat", "hauptchat-thread"}:
|
||||
turns = [t for t in self.conversation.turns if not t.project_id]
|
||||
target_pid = ""
|
||||
label, desc = "Hauptchat", "der Hauptthread (kein Projekt)"
|
||||
else:
|
||||
p = projects_mod.find_project(pname)
|
||||
if not p:
|
||||
return f"Kein Chat/Projekt '{pname}' gefunden (fuer den Hauptthread: name='Hauptchat')."
|
||||
turns = [t for t in self.conversation.turns if t.project_id == p["id"]]
|
||||
target_pid = p["id"]
|
||||
label, desc = p["name"], p.get("description", "(keine Beschreibung)")
|
||||
# VOLLE Historie aus chat_backup.jsonl (nicht nur das rollende
|
||||
# Window — sonst fehlen aeltere/andere Projekt-Chats komplett).
|
||||
turns = self._read_project_history(target_pid, limit=20)
|
||||
if not turns:
|
||||
return (f"'{label}' hat im aktuellen Conversation-Window noch keine "
|
||||
f"Turns. {desc}")
|
||||
tail = turns[-12:]
|
||||
# Fallback aufs In-Memory-Window, falls chat_backup fehlt/leer.
|
||||
turns = [(t.role, t.content) for t in self.conversation.turns
|
||||
if (t.project_id or "") == target_pid]
|
||||
if not turns:
|
||||
return (f"'{label}' hat noch keine Chat-Historie. {desc}")
|
||||
tail = turns[-20:]
|
||||
summary_lines = []
|
||||
for t in tail:
|
||||
prefix = "Stefan" if t.role == "user" else "Du"
|
||||
summary_lines.append(f"{prefix}: {t.content[:280]}")
|
||||
preamble = f"'{label}' — {desc}.\nLetzte {len(tail)} Turns:\n"
|
||||
for role, text in tail:
|
||||
prefix = "Stefan" if role == "user" else "Du"
|
||||
summary_lines.append(f"{prefix}: {(text or '')[:280]}")
|
||||
preamble = f"'{label}' — {desc}.\nLetzte {len(tail)} Turns aus der Historie:\n"
|
||||
return preamble + "\n".join(summary_lines)
|
||||
if name == "project_end":
|
||||
pname = (arguments.get("name") or "").strip()
|
||||
@@ -2602,13 +2653,18 @@ class Agent:
|
||||
kind = (arguments.get("kind") or "").strip().lower()
|
||||
if kind not in ("code", "chat"):
|
||||
return "FEHLER: kind muss 'code' oder 'chat' sein."
|
||||
active_id = projects_mod.get_active()
|
||||
# WICHTIG: das Projekt des AKTUELLEN Requests markieren, NICHT den
|
||||
# globalen active_project — der ist geraeteuebergreifend und wuerde
|
||||
# (bei mehreren App-/Diagnostic-Instanzen) das falsche Projekt
|
||||
# treffen. project_id kommt pro Request vom Client.
|
||||
active_id = (project_id or "").strip()
|
||||
if not active_id:
|
||||
return ("Kein aktives Projekt — Du bist im Hauptthread. Erst ein Projekt "
|
||||
"anlegen/betreten (project_create/project_enter), dann set_project_kind.")
|
||||
return ("Kein aktives Projekt — dieser Chat laeuft im Hauptthread. "
|
||||
"set_project_kind wirkt nur, wenn der Request in einem Projekt "
|
||||
"laeuft (der Client sendet die project_id mit).")
|
||||
updated = projects_mod.update_project(active_id, {"kind": kind})
|
||||
if not updated:
|
||||
return f"FEHLER: aktives Projekt '{active_id}' nicht gefunden."
|
||||
return f"FEHLER: Projekt '{active_id}' nicht gefunden."
|
||||
self._pending_events.append({
|
||||
"type": "project_changed",
|
||||
"project": updated,
|
||||
|
||||
+10
-2
@@ -2007,10 +2007,15 @@ class ARIABridge:
|
||||
elif etype == "project_changed":
|
||||
# ARIA hat ein Projekt erstellt / betreten / verlassen / beendet.
|
||||
# App + Diagnostic refreshen ihren Projekt-Banner anhand des Events.
|
||||
# clientMsgId der ausloesenden Anfrage mitgeben (Text-cmid oder
|
||||
# Voice-audioRequestId) → das ausloesende Geraet kann einen
|
||||
# geraete-lokalen Projektwechsel machen ("geh in Projekt X"),
|
||||
# ohne andere App-/Diagnostic-Instanzen mitzuziehen.
|
||||
await self._send_to_rvs({
|
||||
"type": "project_changed",
|
||||
"payload": {
|
||||
"action": event.get("action") or "",
|
||||
"clientMsgId": client_msg_id or "",
|
||||
**(event.get("project") or {}),
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
@@ -3618,8 +3623,11 @@ class ARIABridge:
|
||||
)
|
||||
|
||||
if meta_action in ("back_to_main", "project_prefix"):
|
||||
# UI-Focus-Update broadcasten
|
||||
payload = {"action": "entered" if meta_action == "project_prefix" else "exited"}
|
||||
# UI-Focus-Update broadcasten — clientMsgId (= audio_request_id der
|
||||
# Aufnahme) mitgeben, damit NUR das Geraet folgt, das den Sprach-
|
||||
# befehl gab (geraete-lokaler Fokus, keine anderen Instanzen ziehen).
|
||||
payload = {"action": "entered" if meta_action == "project_prefix" else "exited",
|
||||
"clientMsgId": audio_request_id or ""}
|
||||
if meta_action == "project_prefix" and project_id:
|
||||
# Namen aus dem Cache holen — best effort
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user