fix(runner): watchdog de timeout indépendant du flux stdout
Le contrôle du deadline vivait dans la boucle for line in proc.stdout : il ne s exécutait qu à l arrivée d une ligne. Un claude qui se tait (outil qui pend) ou qui n exit jamais après son résultat laissait la mission « running » pour toujours et bloquait le nœud (mission f9eb9d1b59c8 du 2026-08-24 : travail fini à 10:38, processus zombie tué à la main à 15:46). Un thread watchdog tue désormais le processus au deadline quoi qu il arrive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 changed file +15 −0
modified
runner/runner.py
+15 −0
@@ -234,6 +234,21 @@ def run_mission(m: MissionIn) -> None: | ||
| 234 | 234 | result_text, cost, turns = "", None, None |
| 235 | 235 | spent_estimate = 0.0 |
| 236 | 236 | deadline = time.time() + m.timeout_seconds |
| 237 | + | |
| 238 | + def _watchdog() -> None: | |
| 239 | + # Filet indépendant du flux stdout : le contrôle du deadline dans | |
| 240 | + # la boucle de lecture ne s'exécute qu'à l'arrivée d'une ligne — | |
| 241 | + # si claude se tait (outil qui pend) ou n'exit jamais après son | |
| 242 | + # résultat (mission f9eb9d1b59c8 du 2026-08-24, zombie 5 h), la | |
| 243 | + # mission restait « running » pour toujours et bloquait le nœud. | |
| 244 | + while proc.poll() is None: | |
| 245 | + if time.time() > deadline: | |
| 246 | + proc.kill() | |
| 247 | + post_event(m.callback_url, {"type": "error", "data": {"error": f"durée max atteinte ({m.timeout_seconds // 60} min) — session interrompue par le watchdog"}}) | |
| 248 | + return | |
| 249 | + time.sleep(30) | |
| 250 | + | |
| 251 | + threading.Thread(target=_watchdog, daemon=True).start() | |
| 237 | 252 | with transcript.open("w") as tf: |
| 238 | 253 | for line in proc.stdout: # type: ignore[union-attr] |
| 239 | 254 | tf.write(line) |
| 240 | 255 | |