Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92e042e91 | ||
|
|
019b17ff97 | ||
|
|
55938c3173 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 20106
|
||||
versionName "0.2.1.6"
|
||||
versionCode 20107
|
||||
versionName "0.2.1.7"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.2.1.6",
|
||||
"version": "0.2.1.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -318,6 +318,9 @@ const ChatScreen: React.FC = () => {
|
||||
const [projectQueues, setProjectQueues] = useState<Record<string, QueuedItem[]>>({});
|
||||
const projectStatesRef = useRef<Record<string, CtxState>>({});
|
||||
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.
|
||||
const projectDraftsRef = useRef<Record<string, string>>({});
|
||||
const prevFocusedPidRef = useRef<string>('');
|
||||
@@ -583,8 +586,24 @@ const ChatScreen: React.FC = () => {
|
||||
try {
|
||||
const s = await brainApi.getProjectQueueStatus();
|
||||
if (cancelled) return;
|
||||
setQueueStatus(s.contexts || {});
|
||||
queueStatusRef.current = s.contexts || {};
|
||||
const ctxs = 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 {}
|
||||
};
|
||||
poll();
|
||||
@@ -918,6 +937,11 @@ const ChatScreen: React.FC = () => {
|
||||
const localOnly = prev.filter(m => {
|
||||
if (m.skillCreated || m.triggerCreated || m.memorySaved) 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)) {
|
||||
// Text-Match-Fallback: wenn der Server irgendwo eine textgleiche
|
||||
// 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) => {
|
||||
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 }));
|
||||
}, []);
|
||||
|
||||
|
||||
+29
-19
@@ -540,10 +540,13 @@ META_TOOLS = [
|
||||
"fires_at": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Absoluter ISO-Timestamp UTC fuer feste Termine, z.B. "
|
||||
"'2026-05-12T14:30:00Z'. Die aktuelle Zeit findest du im "
|
||||
"System-Prompt unter '## Aktuelle Zeit'. Fuer relative Angaben "
|
||||
"lieber `in_seconds` nutzen."
|
||||
"Fester Termin als ISO-Timestamp. Schreib einfach die LOKALE "
|
||||
"Wanduhrzeit, die Stefan meint, OHNE Zeitzone — z.B. 'um 14:30' "
|
||||
"→ '2026-05-12T14:30:00'. Der Server rechnet sie selbst in UTC "
|
||||
"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"},
|
||||
@@ -1042,15 +1045,17 @@ META_TOOLS = [
|
||||
"function": {
|
||||
"name": "project_summary",
|
||||
"description": (
|
||||
"Fasst zusammen was zuletzt in einem Projekt passiert ist (letzte ~10 Turns). "
|
||||
"Nutze zwingend wenn Stefan in ein altes Projekt einsteigt mit "
|
||||
"'hol mich ab' / 'was war zuletzt' / 'erinner mich dran' — sonst "
|
||||
"halluzinierst Du Inhalte die nicht da sind."
|
||||
"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."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Projekt-Name (Fuzzy-Match)."},
|
||||
"name": {"type": "string", "description": "Projekt-Name (Fuzzy-Match) oder 'Hauptchat' fuer den Hauptthread."},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
@@ -2535,22 +2540,27 @@ class Agent:
|
||||
pname = (arguments.get("name") or "").strip()
|
||||
if not pname:
|
||||
return "FEHLER: name ist Pflicht."
|
||||
p = projects_mod.find_project(pname)
|
||||
if not p:
|
||||
return f"Kein Projekt '{pname}' gefunden."
|
||||
# Letzte ~10 Turns des Projekts aus dem Conversation-Log
|
||||
turns = [t for t in self.conversation.turns if t.project_id == p["id"]]
|
||||
# Hauptchat (project_id="") explizit unterstuetzen — damit ARIA auch
|
||||
# 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]
|
||||
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:
|
||||
return (f"Projekt '{p['name']}' existiert (id={p['id']}), aber im "
|
||||
f"aktuellen Conversation-Window stehen noch keine Turns. "
|
||||
f"Beschreibung: {p.get('description', '(keine)')}")
|
||||
return (f"'{label}' hat im aktuellen Conversation-Window noch keine "
|
||||
f"Turns. {desc}")
|
||||
tail = turns[-12:]
|
||||
summary_lines = []
|
||||
for t in tail:
|
||||
prefix = "Stefan" if t.role == "user" else "Du"
|
||||
summary_lines.append(f"{prefix}: {t.content[:280]}")
|
||||
preamble = (f"Projekt '{p['name']}' — {p.get('description', '(keine Beschreibung)')}.\n"
|
||||
f"Letzte {len(tail)} Turns:\n")
|
||||
preamble = f"'{label}' — {desc}.\nLetzte {len(tail)} Turns:\n"
|
||||
return preamble + "\n".join(summary_lines)
|
||||
if name == "project_end":
|
||||
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
|
||||
(z.B. „einiges — geschaeftlich wie privat; frag mich gezielt"), NIEMALS ein
|
||||
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"{local.strftime('%Y-%m-%d %H:%M:%S')} ({local.strftime('%A')})",
|
||||
"",
|
||||
"Nutze das fuer Trigger-Timestamps und um Watcher-Conditions wie "
|
||||
"`hour_of_day == 8` einzuordnen. Fuer relative Angaben "
|
||||
"('in 10min', 'in 2 Stunden') nutze beim `trigger_timer` den "
|
||||
"`in_seconds`-Parameter — Server rechnet dann selbst.",
|
||||
"Nutze das um Watcher-Conditions wie `hour_of_day == 8` einzuordnen. "
|
||||
"Fuer `trigger_timer`: bei relativen Angaben ('in 10min', 'in 2 Stunden') "
|
||||
"den `in_seconds`-Parameter; bei festen Uhrzeiten schreib bei `fires_at` "
|
||||
"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)
|
||||
|
||||
|
||||
+27
-3
@@ -24,7 +24,7 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -40,6 +40,29 @@ def _now_iso() -> str:
|
||||
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:
|
||||
if not isinstance(name, str) or not NAME_RE.match(name):
|
||||
raise ValueError(f"Ungueltiger Trigger-Name: {name!r}")
|
||||
@@ -127,9 +150,10 @@ def create_timer(
|
||||
_safe_name(name)
|
||||
if _path(name).exists():
|
||||
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:
|
||||
datetime.fromisoformat(fires_at_iso.replace("Z", "+00:00"))
|
||||
fires_at_iso = normalize_fires_at_utc(fires_at_iso)
|
||||
except Exception:
|
||||
raise ValueError(f"fires_at_iso ungueltig: {fires_at_iso}")
|
||||
data = {
|
||||
|
||||
Reference in New Issue
Block a user