Skip to content

fix(scheduler): non allertare l'owner su wake non consegnato se l'agente è vivo - #1010

Open
ziomik wants to merge 1 commit into
bradbrok:mainfrom
ziomik:fix/undelivered-alert-live-agent
Open

fix(scheduler): non allertare l'owner su wake non consegnato se l'agente è vivo#1010
ziomik wants to merge 1 commit into
bradbrok:mainfrom
ziomik:fix/undelivered-alert-live-agent

Conversation

@ziomik

@ziomik ziomik commented Aug 6, 2026

Copy link
Copy Markdown

Problema

Quando un wake schedulato non ottiene la ricevuta di consegna entro il timeout, _record_schedule_undelivered paga l'owner con 🚨 FIRED BUT UNDELIVERED — anche quando l'agente è palesemente vivo e sta solo elaborando (turno lungo, chiamata LLM in corso). In quel caso il wake è già stato persistito e viene rigiocato da solo alla sessione successiva: l'alert è puro rumore per l'owner.

Fix

In _record_schedule_undelivered, prima di _queue_owner_alert, si controlla se l'agente ha dato segni di vita negli ultimi 120s. Se sì, si sopprime solo l'alert owner.

Restano invariati:

  • il log FIRED BUT UNDELIVERED su stderr (la diagnostica non si perde);
  • l'activity schedule_undelivered;
  • la persistenza durevole del wake e il replay al boot successivo;
  • il timeout di consegna dei 600s — deliberatamente non toccato: alzarlo nasconderebbe crash reali.

Un agente davvero morto continua a pagare l'owner esattamente come prima.

Scelta della fonte dati (deviazione motivata rispetto al suggerimento iniziale)

Il suggerimento era di usare la stessa fonte di _reconcile_server_liveness / _check_heartbeats, cioè get_latest_heartbeat() + agent.last_seen_at. Ho usato invece registry.get_latest_agent_heartbeat(), per due ragioni:

  1. get_latest_heartbeat() include le righe sintetiche metadata.source='server_presence' che lo scheduler scrive da sé quando vede il transport CONNECTED. Ma il modo di fallire tipico di "FIRED BUT UNDELIVERED" è proprio "transport CONNECTED ma reader loop bloccato su una chiamata LLM": userei come prova di vita una riga scritta dal daemon stesso e nasconderei il crash. È esattamente la distinzione documentata nel docstring di get_latest_agent_heartbeat (review Murzik di feat(api): /admin/force-restart-agent/{name} for wedged agents (#103) #573, endpoint force-restart feat: Replace HEARTBEAT_OK text with send_heartbeat() MCP tool for agent health monitoring #103).
  2. last_seen_at è stampato su consegna inbound riuscita (broker.py:1738), non su risposta dell'agente. Usarlo sarebbe circolare: la consegna del wake stessa potrebbe averlo appena aggiornato, sopprimendo ogni alert.

get_latest_agent_heartbeat applica due tagli — esclude server_presence ed esclude stale/dead — cioè restituisce esattamente "l'agente ha detto lui di essere vivo". È già la convenzione del repo per la domanda "l'agente è davvero responsivo?". Il controllo aggiuntivo su _PROVEN_LIVE_HEARTBEAT_STATUSES (alive/ok/busy/finishing) e il fail-closed su riga assente o errore di lettura completano la garanzia: in dubbio, l'alert parte.

Test (TDD)

Tre nuovi test in tests/test_scheduler.py:

  • test_undelivered_skips_owner_alert_when_agent_proved_live — heartbeat busy recente → nessun alert, ma il wake resta persistito e replayabile. Scritto per primo, verificato rosso prima del fix.
  • test_undelivered_alerts_when_only_server_presence_is_fresh — guardia: una riga server_presence fresca non sopprime l'alert.
  • test_undelivered_alerts_when_agent_heartbeat_is_stale — guardia: heartbeat agent-origin di un'ora fa → alert regolare.

Suite: tests/test_scheduler.py 132 passed; test_daemon.py/test_api.py filtrati su schedule/undelivered verdi; ruff check pulito.

🤖 Opened by Engineer

Un wake schedulato senza ricevuta di consegna paga l'owner con
"FIRED BUT UNDELIVERED" anche quando l'agente e' palesemente vivo e sta
solo elaborando: il wake persistito viene poi rigiocato da solo, quindi
l'alert e' rumore.

_record_schedule_undelivered ora sopprime il SOLO alert owner quando
l'agente ha scritto un heartbeat di origine agente negli ultimi 120s.
Log, activity e persistenza durevole del wake restano invariati, e il
timeout di consegna (600s) non viene toccato: un agente davvero morto
paga l'owner come prima.

Fonte dati: get_latest_agent_heartbeat (non get_latest_heartbeat), che
esclude le righe sintetiche server_presence scritte dallo scheduler
stesso — altrimenti il modo di fallire tipico ("transport CONNECTED ma
reader loop bloccato su una chiamata LLM") userebbe come prova di vita
una riga scritta dal daemon, nascondendo un crash reale. Fail-closed su
riga assente, status non agent-authored o errore di lettura.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant