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:
2026-10-03 10:17:35 +02:00
co-authored by Claude Opus 5
parent e628170760
commit 12b8362d26
4 changed files with 75 additions and 18 deletions
+33 -7
View File
@@ -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()