Files
box/bin/swarm_worker/poller.py
T

139 lines
4.9 KiB
Python

#!/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 <id>`
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()
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)