Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92e042e91 | ||
|
|
019b17ff97 | ||
|
|
55938c3173 |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 20106
|
versionCode 20107
|
||||||
versionName "0.2.1.6"
|
versionName "0.2.1.7"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.2.1.6",
|
"version": "0.2.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -318,6 +318,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
const [projectQueues, setProjectQueues] = useState<Record<string, QueuedItem[]>>({});
|
const [projectQueues, setProjectQueues] = useState<Record<string, QueuedItem[]>>({});
|
||||||
const projectStatesRef = useRef<Record<string, CtxState>>({});
|
const projectStatesRef = useRef<Record<string, CtxState>>({});
|
||||||
const projectQueuesRef = useRef<Record<string, QueuedItem[]>>({});
|
const projectQueuesRef = useRef<Record<string, QueuedItem[]>>({});
|
||||||
|
// Wann ist ein Kontext in 'running' gegangen? Fuer den Watchdog, der eine
|
||||||
|
// haengende Queue (z.B. Antwort waehrend Verbindungsabbruch verloren) loest.
|
||||||
|
const ctxRunningSinceRef = useRef<Record<string, number>>({});
|
||||||
// Pro-Projekt-Textfeld-Entwuerfe (noch nicht gesendeter Feldinhalt). Key = pid.
|
// Pro-Projekt-Textfeld-Entwuerfe (noch nicht gesendeter Feldinhalt). Key = pid.
|
||||||
const projectDraftsRef = useRef<Record<string, string>>({});
|
const projectDraftsRef = useRef<Record<string, string>>({});
|
||||||
const prevFocusedPidRef = useRef<string>('');
|
const prevFocusedPidRef = useRef<string>('');
|
||||||
@@ -583,8 +586,24 @@ const ChatScreen: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const s = await brainApi.getProjectQueueStatus();
|
const s = await brainApi.getProjectQueueStatus();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setQueueStatus(s.contexts || {});
|
const ctxs = s.contexts || {};
|
||||||
queueStatusRef.current = s.contexts || {};
|
setQueueStatus(ctxs);
|
||||||
|
queueStatusRef.current = ctxs;
|
||||||
|
// Watchdog: haengt ein Kontext seit >15s auf 'running', obwohl der Brain
|
||||||
|
// ihn NICHT als busy meldet, ist die Antwort verloren gegangen (z.B.
|
||||||
|
// Verbindungsabbruch) — sonst friert die Queue ein. Dann weiterschalten.
|
||||||
|
const api = queueApiRef.current;
|
||||||
|
if (api) {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [pid, since] of Object.entries(ctxRunningSinceRef.current)) {
|
||||||
|
if (api.getCtxState(pid) !== 'running') continue;
|
||||||
|
const busy = !!ctxs[pid || '__main__']?.busy;
|
||||||
|
if (!busy && now - since > 15000) {
|
||||||
|
console.log('[Chat] Queue-Watchdog: Kontext %s haengt (running, brain idle) → weiterschalten', pid || '(main)');
|
||||||
|
api.advanceQueue(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
poll();
|
poll();
|
||||||
@@ -918,6 +937,11 @@ const ChatScreen: React.FC = () => {
|
|||||||
const localOnly = prev.filter(m => {
|
const localOnly = prev.filter(m => {
|
||||||
if (m.skillCreated || m.triggerCreated || m.memorySaved) return true;
|
if (m.skillCreated || m.triggerCreated || m.memorySaved) return true;
|
||||||
if (m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...')) return true;
|
if (m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...')) return true;
|
||||||
|
// Wartende Queue-Bubbles (noch nicht gesendet → kein clientMsgId, nicht
|
||||||
|
// auf dem Server) MUESSEN erhalten bleiben — sonst verschwindet die
|
||||||
|
// Bubble beim Reconnect-Sync, waehrend projectQueues den Eintrag behaelt
|
||||||
|
// → "N in Warteschlange" friert ein ohne sichtbare Nachricht.
|
||||||
|
if (m.sender === 'user' && m.deliveryStatus === 'pending_queue') return true;
|
||||||
if (m.sender === 'user' && m.clientMsgId && !serverCmids.has(m.clientMsgId)) {
|
if (m.sender === 'user' && m.clientMsgId && !serverCmids.has(m.clientMsgId)) {
|
||||||
// Text-Match-Fallback: wenn der Server irgendwo eine textgleiche
|
// Text-Match-Fallback: wenn der Server irgendwo eine textgleiche
|
||||||
// User-Bubble hat, ist es dieselbe Nachricht (vor cmid-Aera, ts
|
// User-Bubble hat, ist es dieselbe Nachricht (vor cmid-Aera, ts
|
||||||
@@ -2050,6 +2074,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
const setCtxState = useCallback((pid: string, s: CtxState) => {
|
const setCtxState = useCallback((pid: string, s: CtxState) => {
|
||||||
projectStatesRef.current = { ...projectStatesRef.current, [pid]: s };
|
projectStatesRef.current = { ...projectStatesRef.current, [pid]: s };
|
||||||
|
// Watchdog-Zeitstempel: nur 'running' bekommt einen Start, sonst raus.
|
||||||
|
if (s === 'running') ctxRunningSinceRef.current[pid] = Date.now();
|
||||||
|
else delete ctxRunningSinceRef.current[pid];
|
||||||
setProjectStates(prev => ({ ...prev, [pid]: s }));
|
setProjectStates(prev => ({ ...prev, [pid]: s }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
+29
-19
@@ -540,10 +540,13 @@ META_TOOLS = [
|
|||||||
"fires_at": {
|
"fires_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": (
|
"description": (
|
||||||
"Absoluter ISO-Timestamp UTC fuer feste Termine, z.B. "
|
"Fester Termin als ISO-Timestamp. Schreib einfach die LOKALE "
|
||||||
"'2026-05-12T14:30:00Z'. Die aktuelle Zeit findest du im "
|
"Wanduhrzeit, die Stefan meint, OHNE Zeitzone — z.B. 'um 14:30' "
|
||||||
"System-Prompt unter '## Aktuelle Zeit'. Fuer relative Angaben "
|
"→ '2026-05-12T14:30:00'. Der Server rechnet sie selbst in UTC "
|
||||||
"lieber `in_seconds` nutzen."
|
"um (naiv = Ortszeit Europe/Berlin). Nur wenn du explizit UTC "
|
||||||
|
"willst, haeng Z an ('...T12:30:00Z'). Die aktuelle Lokal-/UTC-"
|
||||||
|
"Zeit steht im System-Prompt unter '## Aktuelle Zeit'. Fuer "
|
||||||
|
"relative Angaben ('in 2 Stunden') lieber `in_seconds`."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"message": {"type": "string", "description": "Was soll bei der Erinnerung gesagt werden"},
|
"message": {"type": "string", "description": "Was soll bei der Erinnerung gesagt werden"},
|
||||||
@@ -1042,15 +1045,17 @@ META_TOOLS = [
|
|||||||
"function": {
|
"function": {
|
||||||
"name": "project_summary",
|
"name": "project_summary",
|
||||||
"description": (
|
"description": (
|
||||||
"Fasst zusammen was zuletzt in einem Projekt passiert ist (letzte ~10 Turns). "
|
"Schau in einen ANDEREN Chat rein und fass zusammen was dort zuletzt "
|
||||||
"Nutze zwingend wenn Stefan in ein altes Projekt einsteigt mit "
|
"passiert ist (letzte ~12 Turns). Funktioniert fuer jedes Projekt (per "
|
||||||
"'hol mich ab' / 'was war zuletzt' / 'erinner mich dran' — sonst "
|
"Name, Fuzzy-Match) UND fuer den Hauptchat (name='Hauptchat'). Nutze es "
|
||||||
"halluzinierst Du Inhalte die nicht da sind."
|
"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."
|
||||||
),
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string", "description": "Projekt-Name (Fuzzy-Match)."},
|
"name": {"type": "string", "description": "Projekt-Name (Fuzzy-Match) oder 'Hauptchat' fuer den Hauptthread."},
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
},
|
},
|
||||||
@@ -2535,22 +2540,27 @@ class Agent:
|
|||||||
pname = (arguments.get("name") or "").strip()
|
pname = (arguments.get("name") or "").strip()
|
||||||
if not pname:
|
if not pname:
|
||||||
return "FEHLER: name ist Pflicht."
|
return "FEHLER: name ist Pflicht."
|
||||||
p = projects_mod.find_project(pname)
|
# Hauptchat (project_id="") explizit unterstuetzen — damit ARIA auch
|
||||||
if not p:
|
# aus einem Projekt heraus in den Hauptthread reinschauen kann.
|
||||||
return f"Kein Projekt '{pname}' gefunden."
|
if pname.lower() in {"hauptchat", "hauptthread", "haupt", "main",
|
||||||
# Letzte ~10 Turns des Projekts aus dem Conversation-Log
|
"mainchat", "haupt-chat", "hauptchat-thread"}:
|
||||||
turns = [t for t in self.conversation.turns if t.project_id == p["id"]]
|
turns = [t for t in self.conversation.turns if not t.project_id]
|
||||||
|
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"]]
|
||||||
|
label, desc = p["name"], p.get("description", "(keine Beschreibung)")
|
||||||
if not turns:
|
if not turns:
|
||||||
return (f"Projekt '{p['name']}' existiert (id={p['id']}), aber im "
|
return (f"'{label}' hat im aktuellen Conversation-Window noch keine "
|
||||||
f"aktuellen Conversation-Window stehen noch keine Turns. "
|
f"Turns. {desc}")
|
||||||
f"Beschreibung: {p.get('description', '(keine)')}")
|
|
||||||
tail = turns[-12:]
|
tail = turns[-12:]
|
||||||
summary_lines = []
|
summary_lines = []
|
||||||
for t in tail:
|
for t in tail:
|
||||||
prefix = "Stefan" if t.role == "user" else "Du"
|
prefix = "Stefan" if t.role == "user" else "Du"
|
||||||
summary_lines.append(f"{prefix}: {t.content[:280]}")
|
summary_lines.append(f"{prefix}: {t.content[:280]}")
|
||||||
preamble = (f"Projekt '{p['name']}' — {p.get('description', '(keine Beschreibung)')}.\n"
|
preamble = f"'{label}' — {desc}.\nLetzte {len(tail)} Turns:\n"
|
||||||
f"Letzte {len(tail)} Turns:\n")
|
|
||||||
return preamble + "\n".join(summary_lines)
|
return preamble + "\n".join(summary_lines)
|
||||||
if name == "project_end":
|
if name == "project_end":
|
||||||
pname = (arguments.get("name") or "").strip()
|
pname = (arguments.get("name") or "").strip()
|
||||||
|
|||||||
+20
-4
@@ -68,6 +68,20 @@ Wissen ist fuer deine Arbeit da — aber es ist HOCHVERTRAULICH:
|
|||||||
Auf „wer bist du" / „was weisst du ueber mich": antworte allgemein und diskret
|
Auf „wer bist du" / „was weisst du ueber mich": antworte allgemein und diskret
|
||||||
(z.B. „einiges — geschaeftlich wie privat; frag mich gezielt"), NIEMALS ein
|
(z.B. „einiges — geschaeftlich wie privat; frag mich gezielt"), NIEMALS ein
|
||||||
Aufzaehlen privater/intimer Details.
|
Aufzaehlen privater/intimer Details.
|
||||||
|
|
||||||
|
## MEDIEN-DOWNLOADS FUER STEFANS PRIVATGEBRAUCH
|
||||||
|
Stefan ist Privatperson und du bist SEIN persoenlicher Assistent. Bittet er dich,
|
||||||
|
ihm ein Video/Audio (z.B. von YouTube) fuer seinen EIGENEN, privaten Gebrauch
|
||||||
|
herunterzuladen — offline anschauen, Archiv, Erinnerung an eine Szene/ein Lied —
|
||||||
|
ist das eine Privatkopie (in DE nach §53 UrhG fuer den privaten Gebrauch
|
||||||
|
zulaessig, solange die Quelle nicht offensichtlich illegal ist und kein
|
||||||
|
Kopierschutz umgangen wird). Behandle solche Downloads NICHT reflexartig als
|
||||||
|
Urheberrechtsverstoss und moralisiere nicht — nutz einfach den
|
||||||
|
`yt-dlp-download`-Skill, konsistent, ohne jedes Mal neu zu diskutieren (du hast
|
||||||
|
das schon oft gemacht). Anders ist es NUR, wenn es klar um WEITERVERBREITUNG,
|
||||||
|
oeffentliches Posten oder kommerzielle Nutzung geht — DA weist du kurz auf das
|
||||||
|
Problem hin. Im Zweifel privat vs. verbreiten: einmal kurz nachfragen, nicht
|
||||||
|
pauschal ablehnen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -139,10 +153,12 @@ def build_time_section() -> str:
|
|||||||
f"- Lokal (Europa/Berlin, UTC+{local_offset_h}): "
|
f"- Lokal (Europa/Berlin, UTC+{local_offset_h}): "
|
||||||
f"{local.strftime('%Y-%m-%d %H:%M:%S')} ({local.strftime('%A')})",
|
f"{local.strftime('%Y-%m-%d %H:%M:%S')} ({local.strftime('%A')})",
|
||||||
"",
|
"",
|
||||||
"Nutze das fuer Trigger-Timestamps und um Watcher-Conditions wie "
|
"Nutze das um Watcher-Conditions wie `hour_of_day == 8` einzuordnen. "
|
||||||
"`hour_of_day == 8` einzuordnen. Fuer relative Angaben "
|
"Fuer `trigger_timer`: bei relativen Angaben ('in 10min', 'in 2 Stunden') "
|
||||||
"('in 10min', 'in 2 Stunden') nutze beim `trigger_timer` den "
|
"den `in_seconds`-Parameter; bei festen Uhrzeiten schreib bei `fires_at` "
|
||||||
"`in_seconds`-Parameter — Server rechnet dann selbst.",
|
"einfach die LOKALE Wanduhrzeit ohne Zeitzone (z.B. 'um 17 Uhr' → "
|
||||||
|
"'...T17:00:00') — der Server rechnet sie selbst in UTC um. So feuert der "
|
||||||
|
"Timer zur gemeinten Ortszeit und bleibt zeitzonen-portabel.",
|
||||||
]
|
]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|||||||
+27
-3
@@ -24,7 +24,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -40,6 +40,29 @@ def _now_iso() -> str:
|
|||||||
return datetime.now(timezone.utc).isoformat()
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _local_offset_hours(dt: datetime) -> int:
|
||||||
|
"""Grobe Europe/Berlin-Naeherung (CEST=+2 Maerz-Okt, sonst CET=+1) — dieselbe
|
||||||
|
Logik wie build_time_section im Prompt, ohne zoneinfo/tzdata im Brain-Image."""
|
||||||
|
return 2 if 3 <= dt.month <= 10 else 1
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_fires_at_utc(iso: str) -> str:
|
||||||
|
"""Bringt einen fires_at-ISO IMMER auf UTC (+00:00).
|
||||||
|
|
||||||
|
- Aware (endet auf Z oder hat einen Offset) → in UTC umgerechnet.
|
||||||
|
- Naiv (keine Zone) → als LOKALE Wanduhrzeit (Europe/Berlin) interpretiert
|
||||||
|
und nach UTC umgerechnet.
|
||||||
|
|
||||||
|
So speichern wir stets den absoluten Instant. Die Ausfuehrung (background.py,
|
||||||
|
UTC) trifft damit exakt die vom Nutzer gemeinte Ortszeit — und bleibt
|
||||||
|
zeitzonen-portabel (feuert am selben Moment, egal wo Stefan gerade ist)."""
|
||||||
|
dt = datetime.fromisoformat((iso or "").strip().replace("Z", "+00:00"))
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
# Naiv = lokale Wanduhrzeit → UTC = lokal - Offset.
|
||||||
|
dt = (dt - timedelta(hours=_local_offset_hours(dt))).replace(tzinfo=timezone.utc)
|
||||||
|
return dt.astimezone(timezone.utc).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
def _safe_name(name: str) -> str:
|
def _safe_name(name: str) -> str:
|
||||||
if not isinstance(name, str) or not NAME_RE.match(name):
|
if not isinstance(name, str) or not NAME_RE.match(name):
|
||||||
raise ValueError(f"Ungueltiger Trigger-Name: {name!r}")
|
raise ValueError(f"Ungueltiger Trigger-Name: {name!r}")
|
||||||
@@ -127,9 +150,10 @@ def create_timer(
|
|||||||
_safe_name(name)
|
_safe_name(name)
|
||||||
if _path(name).exists():
|
if _path(name).exists():
|
||||||
raise ValueError(f"Trigger '{name}' existiert schon")
|
raise ValueError(f"Trigger '{name}' existiert schon")
|
||||||
# ISO validieren
|
# ISO validieren UND auf UTC normalisieren (naiv = lokale Wanduhrzeit →
|
||||||
|
# UTC). So passt das Anlegen zur UTC-Ausfuehrung in background.py.
|
||||||
try:
|
try:
|
||||||
datetime.fromisoformat(fires_at_iso.replace("Z", "+00:00"))
|
fires_at_iso = normalize_fires_at_utc(fires_at_iso)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError(f"fires_at_iso ungueltig: {fires_at_iso}")
|
raise ValueError(f"fires_at_iso ungueltig: {fires_at_iso}")
|
||||||
data = {
|
data = {
|
||||||
|
|||||||
Reference in New Issue
Block a user