diff --git a/my-tools/watch_blender_web_scheduler.py b/my-tools/watch_blender_web_scheduler.py index 7578bcd4a..e1ead8182 100755 --- a/my-tools/watch_blender_web_scheduler.py +++ b/my-tools/watch_blender_web_scheduler.py @@ -91,6 +91,18 @@ def checkpoint_task(value: dict) -> str | None: return None +def completed_manifest_for_task(task: str | None) -> bool: + """Return true only when the queue task has an immutable terminal manifest.""" + if not task: + return False + try: + index = json.loads((ROOT / "tests/golden/WBV2/task-index.json").read_text(encoding="utf-8")) + manifest = json.loads((ROOT / "tests/golden" / task / "manifest.json").read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return False + return index.get("activeTask") is None and manifest.get("task") == task and manifest.get("status") == "done" and manifest.get("nextTask") is None + + def authorize_terminal_queue() -> tuple[bool, str]: """Ask the repository authorizer for one source-backed successor batch.""" result = subprocess.run( @@ -153,6 +165,13 @@ def main() -> int: saved_checkpoint = checkpoint() state = saved_checkpoint.get("state") applies = checkpoint_task(saved_checkpoint) == pointer.get("task") + # A failed child can leave a blocked checkpoint behind after the + # module runner has completed its manifest. Treat that checkpoint + # as stale runtime state and let the authorizer select the next + # batch instead of stopping the unattended loop. + if applies and state in {"complete", "blocked", "stopped"} and completed_manifest_for_task(pointer.get("task")): + applies = False + state = None if terminal_pointer(pointer) and args.auto_authorize: authorized, output = authorize_terminal_queue() if authorized: