#!/usr/bin/env python3 """Slot poller for the swarm worker daemon. READ-ONLY component: finds swarm slots waiting for a worker. It never claims slots, never modifies box state, never posts anything. Usage: python3 poller.py # prints pending slots as JSON to stdout from poller import find_pending_slots slots = find_pending_slots() Each returned dict has keys: swarm_id, slot_index, agent_label, task_text, sidechat_id, created_ts """ import json import subprocess import sys from datetime import datetime, timezone # How old (minutes) a running, result-less, unattached slot must be # before we consider it abandoned and re-workable. STALE_MINUTES = 5 # Resolved once at import: bl-native (`box swarm list`) vs legacy (`box swarm-list`). _BOX_FORM = None # "native" | "legacy" def _run_box(*args, timeout=60): """Run a box swarm subcommand, return parsed JSON or None on any failure.""" global _BOX_FORM forms = [] if _BOX_FORM == "native" or _BOX_FORM is None: forms.append(["box", "swarm", *args, "--json"]) if _BOX_FORM == "legacy" or _BOX_FORM is None: # legacy relay style: `box swarm-list`, `box swarm-status ` legacy = "-".join(["swarm"] + list(args[:1])) forms.append(["box", legacy, *args[1:]]) last_err = None for cmd in forms: try: proc = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout ) except (FileNotFoundError, subprocess.TimeoutExpired) as e: last_err = f"{e}" continue if proc.returncode != 0: last_err = f"rc={proc.returncode}: {proc.stderr.strip()[:200]}" # "invalid choice" means wrong form; try the next one. if "invalid choice" in (proc.stderr + proc.stdout): continue print(f"[poller] {' '.join(cmd)} failed: {last_err}", file=sys.stderr) return None try: data = json.loads(proc.stdout) except json.JSONDecodeError as e: print(f"[poller] bad JSON from {' '.join(cmd)}: {e}", file=sys.stderr) return None _BOX_FORM = "native" if cmd[1] == "swarm" else "legacy" return data print(f"[poller] all box forms failed: {last_err}", file=sys.stderr) return None def _age_minutes(ts): """Minutes since an ISO-8601 timestamp; inf when unparseable/missing.""" if not ts: return float("inf") try: dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00")) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return (datetime.now(timezone.utc) - dt).total_seconds() / 60.0 except Exception: return float("inf") def _slot_attached(slot): """True when some worker identity is recorded on the slot.""" return bool(slot.get("subagent_session_id") or slot.get("claimed_by")) def find_pending_slots(stale_minutes=STALE_MINUTES): """Return slots waiting for a worker. Read-only; claims nothing. A slot qualifies when: - its status is "pending", or it has no agent assigned, OR - it is "running" with result null, older than `stale_minutes`, and no worker identity is attached. """ found = [] data = _run_box("list") if not data: return found swarms = data.get("swarms", []) if isinstance(data, dict) else [] for summary in swarms: if (summary.get("status") or "").lower() != "running": continue swarm_id = summary.get("swarm_id") if not swarm_id: continue detail = _run_box("status", swarm_id) if not detail: continue swarm = detail.get("swarm", {}) if isinstance(detail, dict) else {} task_text = swarm.get("task") or summary.get("task_preview") or "" created_ts = swarm.get("created_ts") or summary.get("created_ts") for slot in swarm.get("slots", []): status = (slot.get("status") or "").lower() if status in ("done", "failed", "killed"): continue result = slot.get("result") agent = slot.get("agent_id") pending = (status == "pending") or (not agent) stale_running = ( status == "running" and result is None and _age_minutes(slot.get("updated_ts")) > stale_minutes and not _slot_attached(slot) ) if pending or stale_running: found.append({ "swarm_id": swarm_id, "slot_index": slot.get("slot"), "agent_label": agent, "task_text": task_text, "sidechat_id": slot.get("sidechat_id") or slot.get("thread_uuid"), "created_ts": created_ts, }) return found if __name__ == "__main__": slots = find_pending_slots() print(json.dumps(slots, indent=1)) print(f"[poller] {len(slots)} pending slot(s) found", file=sys.stderr)