fix(trigger): Antwort landet im Projekt des Timers — und der Zustand luegt nicht mehr
Zwei Fehler, die zusammen wie "der Timer hat nicht gefeuert" aussahen. 1) Trigger hatten KEIN Projekt. Beim Feuern ging die Antwort ohne projectId durch _process_core_response — also immer in den Hauptchat, auch wenn der Timer mitten in einem Projekt-Thread gestellt wurde. Im Projekt blieb es still, und es sah aus wie "nicht gefeuert". Jetzt wird das Projekt des Requests mit angelegt, der getriggerte Turn laeuft darin, und die Antwort kommt dort zurueck. 2) trigger_list zeigte nur "aktiv" / "DEAKTIVIERT" und verschwieg fire_count und last_fired_at. Ein One-Shot-Timer wird NACH dem Feuern automatisch deaktiviert — "hat sauber gefeuert" und "war nie an" sahen damit identisch aus. ARIA hat genau das falsch gelesen und berichtet, sie habe vergessen den Timer zu aktivieren, obwohl er puenktlich gefeuert hatte. Die Ausgabe nennt jetzt ausdruecklich "SCHON GEFEUERT (Nx, zuletzt <Zeit>)" plus den Kontext, in dem die Antwort landet. Nachgewiesen am Fall flugwetter-whv-freitag-0900: gefeuert 07:00:05 UTC (= 09:00 lokal), Antwort 46s spaeter, fire_count=1 — aber active=false im Manifest und die Antwort im Hauptchat statt im Projekt-Thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-7
@@ -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()
|
||||
|
||||
@@ -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]})
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
|
||||
|
||||
+15
-5
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user