diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 6ff163d..b9a858d 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -3246,8 +3246,13 @@ class Agent: if name in _UI_ACTIONS: action, keys = _UI_ACTIONS[name] params = {k: arguments[k] for k in keys if arguments.get(k) is not None} + # Sprachausgabe braucht laenger: der Agent meldet erst Erfolg, + # wenn wirklich Ton laeuft (TTS-Render + Uebertragung). Mit den + # 20s der UI-Aktionen lief die ehrliche Antwort in den Timeout. + _timeout = 40 if action in ("speak", "notify") else 20 result = _post("/internal/host", - {"host": host, "action": action, "params": params}, 20) + {"host": host, "action": action, "params": params}, + _timeout) if not result.get("ok"): return f"FEHLER: {result.get('error')}" r = result.get("result") or {} @@ -3512,6 +3517,10 @@ class Agent: fires_at_iso=fires_at_iso, message=arguments["message"], author="aria", + # Projekt DIESES Requests mitnehmen: beim Feuern soll die + # Antwort dort landen, wo der Timer gestellt wurde — nicht + # im Hauptchat. + project_id=project_id or "", ) self._pending_events.append({ "type": "trigger_created", @@ -3527,6 +3536,7 @@ class Agent: check_interval_sec=int(arguments.get("check_interval_sec", 300)), throttle_sec=int(arguments.get("throttle_sec", 3600)), author="aria", + project_id=project_id or "", ) self._pending_events.append({ "type": "trigger_created", @@ -3599,13 +3609,29 @@ class Agent: return "(keine Trigger vorhanden)" lines = [] for t in items: - state = "aktiv" if t.get("active", True) else "DEAKTIVIERT" - if t["type"] == "timer": - lines.append(f"- {t['name']} (timer, {state}): feuert {t.get('fires_at')} — \"{t.get('message','')[:50]}\"") - elif t["type"] == "watcher": - lines.append(f"- {t['name']} (watcher, {state}): cond=\"{t.get('condition')}\", throttle={t.get('throttle_sec')}s") + # WICHTIG: ein One-Shot-Timer wird NACH dem Feuern automatisch + # deaktiviert. Stand hier nur "DEAKTIVIERT", sahen "hat sauber + # gefeuert" und "war nie an" identisch aus — und genau das hat + # schon zu einer falschen Auskunft gefuehrt ("ich habe + # vergessen ihn zu aktivieren", obwohl er gefeuert hatte). + # Darum: gefeuert IMMER explizit mit Zeitpunkt und Zaehler. + fired_n = int(t.get("fire_count", 0) or 0) + last_fired = t.get("last_fired_at") or "" + if fired_n > 0: + state = (f"SCHON GEFEUERT ({fired_n}x, zuletzt {last_fired})" + + ("" if t.get("active", True) else ", danach automatisch deaktiviert")) + elif t.get("active", True): + state = "aktiv, noch nicht gefeuert" else: - lines.append(f"- {t['name']} ({t['type']}, {state})") + state = "DEAKTIVIERT, nie gefeuert" + # Wohin die Antwort beim Feuern geht — leer = Hauptchat. + where = f" [Kontext: {t.get('project_id') or 'Hauptchat'}]" + if t["type"] == "timer": + lines.append(f"- {t['name']} (timer, {state}){where}: feuert {t.get('fires_at')} — \"{t.get('message','')[:50]}\"") + elif t["type"] == "watcher": + lines.append(f"- {t['name']} (watcher, {state}){where}: cond=\"{t.get('condition')}\", throttle={t.get('throttle_sec')}s") + else: + lines.append(f"- {t['name']} ({t['type']}, {state}){where}") return "\n".join(lines) if name == "oauth_register_provider": svc = (arguments.get("service") or "").strip() diff --git a/aria-brain/background.py b/aria-brain/background.py index 395c632..0691360 100644 --- a/aria-brain/background.py +++ b/aria-brain/background.py @@ -36,7 +36,8 @@ TICK_SEC = 8 BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090") -def _push_to_bridge(reply: str, trigger_name: str, ttype: str, events: list) -> None: +def _push_to_bridge(reply: str, trigger_name: str, ttype: str, events: list, + project_id: str = "") -> None: """POSTed eine Trigger-Antwort an die Bridge fuer RVS-Broadcast + TTS. Synchron via urllib — wird per run_in_executor aus dem async-Loop @@ -47,6 +48,10 @@ def _push_to_bridge(reply: str, trigger_name: str, ttype: str, events: list) -> "trigger_name": trigger_name, "type": ttype, "events": events or [], + # Kontext, in dem der Trigger angelegt wurde. Leer = Hauptchat. Ohne das + # landete JEDE Trigger-Antwort im Hauptchat, auch wenn der Timer mitten + # in einem Projekt-Thread gestellt wurde. + "projectId": project_id or "", }).encode("utf-8") url = f"{BRIDGE_URL}/internal/trigger-fired" try: @@ -156,17 +161,23 @@ async def _fire(trigger: dict, agent_factory) -> None: # Executor auslagern, damit der Event-Loop frei bleibt. loop = asyncio.get_running_loop() + # Projekt, in dem der Trigger angelegt wurde — der Turn laeuft darin, und + # die Antwort geht auch dorthin zurueck (statt immer in den Hauptchat). + trigger_pid = str(trigger.get("project_id") or "") + def _run_turn(): a = agent_factory() - rep, *_rest = a.chat(prompt, source="trigger") + rep, *_rest = a.chat(prompt, source="trigger", project_id=trigger_pid) return rep, a.pop_events() reply, events = await loop.run_in_executor(None, _run_turn) - logger.info("[trigger] %s gefeuert → ARIA-Reply: %s", name, reply[:80]) + logger.info("[trigger] %s gefeuert (project=%s) → ARIA-Reply: %s", + name, trigger_pid or "(main)", reply[:80]) triggers_mod.append_log(name, {"event": "reply", "text": reply[:500]}) # Reply an die Bridge pushen, damit App + Diagnostic + TTS sie kriegen. # Ohne diesen Push wuerde die Antwort nur im Brain-Log landen. - await loop.run_in_executor(None, _push_to_bridge, reply, name, ttype, events) + await loop.run_in_executor(None, _push_to_bridge, reply, name, ttype, + events, trigger_pid) except Exception as e: logger.exception("Trigger %s feuern fehlgeschlagen: %s", name, e) triggers_mod.append_log(name, {"event": "error", "error": str(e)[:300]}) diff --git a/aria-brain/triggers.py b/aria-brain/triggers.py index abf4e27..0955eb0 100644 --- a/aria-brain/triggers.py +++ b/aria-brain/triggers.py @@ -146,6 +146,7 @@ def create_timer( fires_at_iso: str, message: str, author: str = "aria", + project_id: str = "", ) -> dict: _safe_name(name) if _path(name).exists(): @@ -164,11 +165,16 @@ def create_timer( "created_at": _now_iso(), "fires_at": fires_at_iso, "message": message, + # Projekt, in dem der Trigger angelegt wurde. Ohne das landete die + # Antwort beim Feuern IMMER im Hauptchat — auch wenn der Timer mitten + # in einem Projekt-Thread gestellt wurde. Leer = Hauptchat. + "project_id": project_id or "", "fire_count": 0, "last_fired_at": None, } write(name, data) - logger.info("Trigger angelegt: %s (timer, fires_at=%s)", name, fires_at_iso) + logger.info("Trigger angelegt: %s (timer, fires_at=%s, project=%s)", + name, fires_at_iso, project_id or "(main)") return data @@ -179,6 +185,7 @@ def create_watcher( check_interval_sec: int = 300, throttle_sec: int = 3600, author: str = "aria", + project_id: str = "", ) -> dict: _safe_name(name) if _path(name).exists(): @@ -200,12 +207,15 @@ def create_watcher( "check_interval_sec": int(check_interval_sec), "throttle_sec": int(throttle_sec), "message": message, + # Siehe create_timer: ohne Projekt landet die Antwort im Hauptchat. + "project_id": project_id or "", "fire_count": 0, "last_fired_at": None, "last_checked_at": None, } write(name, data) - logger.info("Trigger angelegt: %s (watcher, cond='%s')", name, condition) + logger.info("Trigger angelegt: %s (watcher, cond='%s', project=%s)", + name, condition, project_id or "(main)") return data diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index e650c6b..b32c9a1 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -4618,11 +4618,15 @@ class ARIABridge: trigger_name = data.get("trigger_name", "") ttype = data.get("type", "trigger") events = data.get("events") or [] - logger.info("[bridge ← brain] Trigger '%s' (%s) gefeuert, reply=%d chars, events=%d", - trigger_name, ttype, len(reply), len(events)) + trigger_pid = str(data.get("projectId") or "") + logger.info("[bridge ← brain] Trigger '%s' (%s) gefeuert, reply=%d chars, " + "events=%d, project=%s", + trigger_name, ttype, len(reply), len(events), + trigger_pid or "(main)") # Async-spawn — HTTP-Antwort nicht durch RVS-Broadcast blockieren asyncio.create_task( - self._handle_trigger_fired(reply, trigger_name, ttype, events) + self._handle_trigger_fired(reply, trigger_name, ttype, events, + trigger_pid) ) await _send_response(writer, 200, {"ok": True}) elif method == "POST" and path == "/internal/agent-activity": @@ -5233,7 +5237,8 @@ class ARIABridge: return {"ok": True, "role": role, "content_preview": content[:80]} async def _handle_trigger_fired(self, reply: str, trigger_name: str, - ttype: str, events: list) -> None: + ttype: str, events: list, + project_id: str = "") -> None: """Spiegelt eine Brain-Trigger-Antwort wie eine normale ARIA-Antwort. Side-Channel-Events zuerst (trigger_created, location_tracking, ...), @@ -5285,7 +5290,12 @@ class ARIABridge: try: await self._process_core_response( reply, - {"metadata": {"trigger_name": trigger_name, "trigger_type": ttype}}, + {"metadata": {"trigger_name": trigger_name, "trigger_type": ttype}, + # Ohne projectId landete jede Trigger-Antwort im Hauptchat — + # auch wenn der Timer in einem Projekt-Thread gestellt wurde. + # Stefan sah dann im Projekt nichts und hielt den Timer fuer + # nicht gefeuert. + "projectId": project_id or ""}, ) except Exception: logger.exception("[trigger-fire] _process_core_response fehlgeschlagen")