fix(agent_v1): keepalive headless quand pystray ne peut pas tenir le main thread
Symptome (3 incidents 24h les 24/05) : apres relance distante de Lea via SSH, les polls /replay/next repartent un moment puis s'arretent. Diagnostic : - agent_v1/ui/smart_tray.py:875 utilise pystray.Icon.run() comme boucle principale - main.py:132-133 lance _replay_poll_loop et _background_heartbeat_loop en daemon threads - Quand Lea est lancee via sshpass sans session interactive Windows, pystray echoue (pas de systray accessible) et icon.run() sort - agent.run() retourne, main() retourne, main thread termine - Les daemon threads meurent avec le main thread (par design Python) Fix : _headless_keepalive() maintient le main thread vivant via threading.Event quand agent.run() sort en laissant agent.running=True (cas anormal). Handlers SIGTERM/SIGINT/SIGBREAK pour shutdown propre. Invisible en mode interactif normal (icon.run() ne sort jamais). Pas de modification de smart_tray ni de la cascade visuelle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -571,9 +571,67 @@ class AgentV1:
|
|||||||
def run(self):
|
def run(self):
|
||||||
self.ui.run()
|
self.ui.run()
|
||||||
|
|
||||||
|
def _headless_keepalive(agent):
|
||||||
|
"""Maintient le main thread vivant quand l'UI tray ne peut pas tourner.
|
||||||
|
|
||||||
|
Sans cela, ``agent.run()`` retourne immédiatement (pystray échoue quand
|
||||||
|
Léa est lancée via SSH sans session interactive Windows), le main thread
|
||||||
|
se termine, et TOUS les daemon threads — y compris ``_replay_poll_loop``
|
||||||
|
— meurent avec lui. Observé 3 fois en 24h les 24/05 :
|
||||||
|
- SSH ``Permission denied`` (1231)
|
||||||
|
- polls morts après relance distante (1620)
|
||||||
|
- polls morts ``replay_sess_506d6fa2`` (1627)
|
||||||
|
|
||||||
|
Le keepalive ne se déclenche QUE si ``agent.run()`` est sorti tout en
|
||||||
|
laissant ``agent.running=True`` (cas anormal). En mode interactif
|
||||||
|
normal, ``pystray.Icon.run()`` ne sort jamais, donc ce code est
|
||||||
|
invisible.
|
||||||
|
"""
|
||||||
|
import signal as _sig
|
||||||
|
_stop = threading.Event()
|
||||||
|
|
||||||
|
def _handler(sig, frame):
|
||||||
|
logger.info(f"[MAIN] Signal {sig} recu — arret propre")
|
||||||
|
_stop.set()
|
||||||
|
agent.running = False
|
||||||
|
|
||||||
|
for sig_name in ("SIGTERM", "SIGINT", "SIGBREAK"):
|
||||||
|
sig_obj = getattr(_sig, sig_name, None)
|
||||||
|
if sig_obj is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
_sig.signal(sig_obj, _handler)
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[MAIN] Keepalive headless actif — main thread bloque pour maintenir "
|
||||||
|
"les daemon threads (_replay_poll_loop, heartbeat, capture_server) vivants. "
|
||||||
|
"Pour stopper Lea : kill -TERM <pid> ou Ctrl+C."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
_stop.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
agent.running = False
|
||||||
|
logger.info("[MAIN] Keepalive termine — agent.running=False, daemon threads vont s'arreter")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
agent = AgentV1()
|
agent = AgentV1()
|
||||||
agent.run()
|
try:
|
||||||
|
agent.run()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("[MAIN] agent.run() a leve une exception")
|
||||||
|
|
||||||
|
if getattr(agent, "running", False):
|
||||||
|
logger.warning(
|
||||||
|
"[MAIN] agent.run() est sorti mais agent.running=True — "
|
||||||
|
"probablement pystray sans session interactive (SSH). "
|
||||||
|
"Bascule en keepalive headless."
|
||||||
|
)
|
||||||
|
_headless_keepalive(agent)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user