fix(trigger): near() fired bei Auto-Vorbeifahrten verpasst — Loop schneller + event-getrieben

Stefan ist mehrmals an einem 300m-near()-Watcher (DRK Kreyenbrueck)
vorbeigefahren, kein Fire. Ursache: Background-Loop tickte alle 30s,
Auto-Durchfahrt durch 600m-Durchmesser-Radius dauert bei 50-120 km/h
nur 18-43 Sekunden — der Tick konnte komplett dazwischen liegen.

Drei Fixes (A + B aus Stefans Vorschlag):

A1. Background-Loop-Frequenz: TICK_SEC 30 → 8.
    Garantiert mind. 2 Checks auch bei 120 km/h durch 300m. Loop ist
    billig (paar Dateilesungen + AST-Eval), Brain merkt das nicht.

A2. near() bekommt Age-Schutz (watcher.py NEAR_MAX_AGE_SEC=300):
    Wenn location_age_sec > 5 min, gilt die Position als unbekannt
    und near() liefert False. Verhindert Phantom-Fires wenn Tracking
    aus ist oder Mobilfunk weg war — vorher haette der letzte
    bekannte Wert weiter ausgewertet werden koennen.

B. Event-getriebener Tick:
    - background.py: tick_now()-Funktion + Module-Slot fuer
      agent_factory damit man von ausserhalb des Lifespan-Pfads
      einen Tick triggern kann
    - main.py: POST /triggers/check-now Endpoint ruft tick_now()
    - bridge: _persist_location feuert nach jedem Save ein fire-and-
      forget POST /triggers/check-now (run_in_executor, timeout 8s,
      blockt nichts wenn Brain stockt)

Damit fires near() sofort wenn die App ein location_update schickt —
Polling ist nur noch der Fallback fuer Watcher OHNE GPS-Bezug
(disk_free, hour_of_day etc.) und als Sicherheits-Tick falls
location_update mal ausfaellt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 18:16:53 +02:00
parent 0fcbf5e3ed
commit 6f80e442cf
4 changed files with 81 additions and 3 deletions
+29 -1
View File
@@ -27,7 +27,12 @@ import watcher as watcher_mod
logger = logging.getLogger(__name__)
TICK_SEC = 30
# Polling-Frequenz des Background-Loops. Vorher 30s → Auto-Vorbeifahrt
# durch einen 300m-Radius bei >50 km/h konnte zwischen zwei Ticks komplett
# verpasst werden. Mit 8s ist auch eine 18-Sekunden-Durchfahrt (120 km/h
# durch 300m) garantiert mind. einmal getroffen. Der Loop ist billig
# (paar Dateilesungen + AST-Eval), das macht Brain nicht warm.
TICK_SEC = 8
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
@@ -195,8 +200,31 @@ async def _tick(agent_factory) -> None:
logger.warning("Trigger-Check %s: %s", trigger.get("name"), e)
# Module-Level-Slot fuer die agent_factory damit on-demand-Ticks (von
# z.B. POST /triggers/check-now) Zugang haben ohne durch den ganzen
# Lifespan-Pfad geschleust zu werden.
_AGENT_FACTORY = None
async def tick_now() -> dict:
"""Sofortiger Trigger-Check — nicht warten auf den naechsten Loop-Tick.
Wird genutzt wenn ein neues GPS-Update reinkommt: Bridge ruft das nach
_persist_location, damit Watcher mit near() den frischen Wert sofort
sehen statt bis zu TICK_SEC Sekunden zu warten."""
if _AGENT_FACTORY is None:
return {"ok": False, "error": "Background-Loop noch nicht gestartet"}
try:
await _tick(_AGENT_FACTORY)
return {"ok": True}
except Exception as exc:
logger.exception("tick_now: %s", exc)
return {"ok": False, "error": str(exc)}
async def run_loop(agent_factory) -> None:
"""Endlosschleife — wird vom main lifespan gestartet + gestoppt."""
global _AGENT_FACTORY
_AGENT_FACTORY = agent_factory
logger.info("Trigger-Loop gestartet (TICK_SEC=%d)", TICK_SEC)
while True:
try: