Erinaldordso commited on
Commit
1b2dd50
·
verified ·
1 Parent(s): 62e7ce0

fix: bound the waits that could freeze the learn loop (voyager_runner.py)

Browse files
Files changed (1) hide show
  1. voyager_runner.py +47 -0
voyager_runner.py CHANGED
@@ -5,6 +5,7 @@ import json
5
  import logging
6
  import shutil
7
  import sys
 
8
  import time
9
  from pathlib import Path
10
 
@@ -314,10 +315,56 @@ def main() -> None:
314
  delay = float(env("VOYAGER_LEARN_BACKOFF_SECONDS", "15"))
315
  delay_cap = float(env("VOYAGER_LEARN_BACKOFF_CAP_SECONDS", "300"))
316
  attempt = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  try:
318
  quarantine_invalid_skills(voyager, ckpt)
319
  quarantine_template_skills(voyager, ckpt)
320
  quarantine_placeholder_skills(voyager, ckpt)
 
321
  while True:
322
  attempt += 1
323
  try:
 
5
  import logging
6
  import shutil
7
  import sys
8
+ import threading
9
  import time
10
  from pathlib import Path
11
 
 
315
  delay = float(env("VOYAGER_LEARN_BACKOFF_SECONDS", "15"))
316
  delay_cap = float(env("VOYAGER_LEARN_BACKOFF_CAP_SECONDS", "300"))
317
  attempt = 0
318
+
319
+ # A watchdog for the failure mode that retry-forever hides: the process is
320
+ # alive, every error counter is zero, and nothing progresses because
321
+ # learn() is parked on a wedged dependency. Retrying cannot help there --
322
+ # the loop is blocked, not failing -- so the only recovery is to exit and
323
+ # let the supervisor start a fresh runner. Measured: the bot sat in that
324
+ # state for 45+ minutes with zero model calls. Progress is read from the
325
+ # checkpoint files (not from a log line), so it is immune to the log being
326
+ # absent, and the watchdog only arms once a task has actually completed,
327
+ # so a cold start is never killed.
328
+ stall_seconds = float(env("VOYAGER_STALL_SECONDS", "900"))
329
+ completed_path = ckpt / "curriculum" / "completed_tasks.json"
330
+
331
+ def _progress_fingerprint():
332
+ try:
333
+ stat = completed_path.stat()
334
+ return (stat.st_mtime_ns, stat.st_size)
335
+ except OSError:
336
+ return None
337
+
338
+ def _watch_stall():
339
+ last_seen = _progress_fingerprint()
340
+ last_change = time.time()
341
+ while True:
342
+ time.sleep(60)
343
+ current = _progress_fingerprint()
344
+ if current != last_seen:
345
+ last_seen = current
346
+ last_change = time.time()
347
+ continue
348
+ idle = time.time() - last_change
349
+ if idle >= stall_seconds:
350
+ # os._exit is deliberate: the learning thread is blocked inside
351
+ # a call that will not return, so a cooperative shutdown would
352
+ # itself hang. Exiting hard lets the Space watchdog observe it.
353
+ print(
354
+ f"[voyager] STALL WATCHDOG: no completed task in {idle:.0f}s "
355
+ f"(limit {stall_seconds:.0f}s); exiting so the supervisor can "
356
+ f"restart the runner",
357
+ flush=True,
358
+ )
359
+ sys.stdout.flush()
360
+ sys.stderr.flush()
361
+ os._exit(75)
362
+
363
  try:
364
  quarantine_invalid_skills(voyager, ckpt)
365
  quarantine_template_skills(voyager, ckpt)
366
  quarantine_placeholder_skills(voyager, ckpt)
367
+ threading.Thread(target=_watch_stall, daemon=True, name="voyager-stall-watch").start()
368
  while True:
369
  attempt += 1
370
  try: