chore(fleet): sync operator memory, hatch menu dialogs, and watchdog alerts
This commit is contained in:
+64
-14
@@ -30,8 +30,9 @@ sys.path.insert(0, _SWARM_DIR)
|
||||
sys.path.insert(0, _BIN_DIR)
|
||||
|
||||
from poller import find_pending_slots
|
||||
from executor import execute_task, _looks_like_shell
|
||||
from executor import execute_task, _looks_like_shell, extract_shell_command, execute_task_in_tmux
|
||||
from reporter import post_result, attach_slot
|
||||
from mainloop_notify import notify_via_mainloop
|
||||
|
||||
# Fast gateway integration
|
||||
try:
|
||||
@@ -49,7 +50,7 @@ except ImportError:
|
||||
|
||||
POLL_INTERVAL = 60 # seconds between poll cycles
|
||||
STALE_MINUTES = 5 # slots older than this with no attach are workable
|
||||
WORKER_POOL = ["dev", "def", "muse"]
|
||||
WORKER_POOL = ["muse"] # only dispatch to fully authenticated agent nodes
|
||||
|
||||
# === SAFETY SWITCH ===
|
||||
# True -> observe only: log what WOULD be done, execute/post nothing.
|
||||
@@ -65,12 +66,12 @@ log = logging.getLogger("swarm-worker")
|
||||
|
||||
|
||||
def _select_worker(preferred=None):
|
||||
if preferred and HAS_MUSE_HYBRID and muse_hybrid.is_node_configured(preferred):
|
||||
if preferred and preferred in WORKER_POOL and HAS_MUSE_HYBRID and muse_hybrid.is_node_configured(preferred):
|
||||
return preferred
|
||||
for candidate in WORKER_POOL:
|
||||
if HAS_MUSE_HYBRID and muse_hybrid.is_node_configured(candidate):
|
||||
return candidate
|
||||
return preferred or "dev"
|
||||
return "muse"
|
||||
|
||||
|
||||
def process_slot(slot):
|
||||
@@ -81,19 +82,56 @@ def process_slot(slot):
|
||||
agent_label = slot.get("agent_label")
|
||||
sidechat_id = slot.get("sidechat_id")
|
||||
tag = "%s/%s" % (swarm_id, slot_index)
|
||||
short_id = swarm_id[3:19] if str(swarm_id).startswith("sw-") else str(swarm_id)[:16]
|
||||
session_name = f"sw-{short_id}-s{slot_index}"
|
||||
|
||||
if DRY_RUN:
|
||||
log.info("[dry-run] would execute slot %s (agent=%s, task %.80r)",
|
||||
tag, agent_label, task_text)
|
||||
return True
|
||||
|
||||
# If the task is NOT a shell command, dispatch it to an ephemeral Muse subagent.
|
||||
is_shell = _looks_like_shell(task_text)
|
||||
if not is_shell and HAS_MUSE_HYBRID:
|
||||
# 1. Check if the task is or contains an executable shell command
|
||||
cmd = extract_shell_command(task_text)
|
||||
if cmd:
|
||||
log.info("executing slot %s in host tmux session %s on bl", tag, session_name)
|
||||
# Attach/claim slot in box state
|
||||
attach_slot(swarm_id, slot_index, "swarm-worker", session_id=session_name)
|
||||
try:
|
||||
result = execute_task_in_tmux(session_name, cmd)
|
||||
except Exception:
|
||||
log.error("tmux executor crashed on slot %s:\n%s", tag, traceback.format_exc())
|
||||
result = {"success": False, "output": "",
|
||||
"error": "tmux executor crashed: see worker log"}
|
||||
|
||||
payload = {
|
||||
"ok": bool(result.get("success")),
|
||||
"output": result.get("output") or "",
|
||||
"error": result.get("error"),
|
||||
}
|
||||
try:
|
||||
posted = post_result(swarm_id, slot_index, payload)
|
||||
except Exception:
|
||||
log.error("reporter crashed on slot %s:\n%s", tag, traceback.format_exc())
|
||||
posted = False
|
||||
|
||||
# Post completion note to the slot sidechat for main loop visibility
|
||||
summary_msg = payload.get("output") or payload.get("error") or "completed"
|
||||
try:
|
||||
notified = notify_via_mainloop(swarm_id, slot_index, summary_msg, worker_id="swarm-worker")
|
||||
log.info("slot %s sidechat notification: %s", tag, notified)
|
||||
except Exception as ne:
|
||||
log.warning("failed to post sidechat notification for %s: %s", tag, ne)
|
||||
|
||||
log.info("slot %s done: ok=%s posted=%s (%.1fs)",
|
||||
tag, payload["ok"], posted,
|
||||
float(result.get("duration_s") or 0.0))
|
||||
return bool(payload["ok"]) and posted
|
||||
|
||||
# 2. If the task is purely prose/instructions, dispatch to a verified agent subagent
|
||||
if HAS_MUSE_HYBRID:
|
||||
worker_agent = _select_worker(agent_label)
|
||||
log.info("dispatching subagent slot %s to %s", tag, worker_agent)
|
||||
log.info("dispatching prose subagent slot %s to %s", tag, worker_agent)
|
||||
try:
|
||||
# 1. Start an ephemeral subagent session
|
||||
title = f"sw-{swarm_id[:16]}-s{slot_index}"
|
||||
sess, err = muse_hybrid.start_session(worker_agent, title=title)
|
||||
if err or not sess or not sess.get("session_id"):
|
||||
@@ -103,12 +141,19 @@ def process_slot(slot):
|
||||
sub_sid = sess["session_id"]
|
||||
log.info("subagent session %s created for slot %s on %s", sub_sid, tag, worker_agent)
|
||||
|
||||
# 2. Attach/claim the slot in box state with subagent session_id
|
||||
# Attach/claim slot in box state
|
||||
attached = attach_slot(swarm_id, slot_index, worker_agent, session_id=sub_sid)
|
||||
if not attached:
|
||||
log.warning("failed to attach slot %s to %s; proceeding with dispatch", tag, worker_agent)
|
||||
|
||||
# 3. Format prompt with authentic Operator Directive and RESULT expectation
|
||||
# Register in subagent_tracker
|
||||
try:
|
||||
import subagent_tracker
|
||||
subagent_tracker.register_session(worker_agent, sub_sid, title=title, prompt=task_text[:200])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Format prompt with authentic Operator Directive and RESULT expectation
|
||||
if HAS_PROMPT_ENVELOPE and hasattr(prompt_envelope, "wrap_subagent_task"):
|
||||
prompt_body = prompt_envelope.wrap_subagent_task(tag, task_text)
|
||||
else:
|
||||
@@ -122,7 +167,7 @@ def process_slot(slot):
|
||||
f"(or [RESULT {tag}] FAIL: <reason> if the task could not be completed)\n"
|
||||
)
|
||||
|
||||
# 4. Asynchronously send message to subagent session
|
||||
# Asynchronously send message to subagent session
|
||||
res, send_err = muse_hybrid.send_message(worker_agent, prompt_body, thread_id=sub_sid, wait=0)
|
||||
if send_err:
|
||||
log.error("failed to send task to subagent %s on %s: %s", sub_sid, worker_agent, send_err)
|
||||
@@ -134,8 +179,8 @@ def process_slot(slot):
|
||||
log.error("subagent dispatch crashed on slot %s:\n%s", tag, traceback.format_exc())
|
||||
return False
|
||||
|
||||
# Otherwise fallback to sandboxed host execution
|
||||
log.info("executing slot %s in sandbox (agent=%s)", tag, agent_label)
|
||||
# 3. Fallback to sandboxed host execution
|
||||
log.info("executing slot %s in fallback sandbox (agent=%s)", tag, agent_label)
|
||||
try:
|
||||
result = execute_task(task_text)
|
||||
except Exception:
|
||||
@@ -154,6 +199,11 @@ def process_slot(slot):
|
||||
log.error("reporter crashed on slot %s:\n%s", tag, traceback.format_exc())
|
||||
posted = False
|
||||
|
||||
try:
|
||||
notify_via_mainloop(swarm_id, slot_index, payload.get("output") or "done", worker_id="swarm-worker")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
log.info("slot %s done: ok=%s posted=%s (%.1fs)",
|
||||
tag, payload["ok"], posted,
|
||||
float(result.get("duration_s") or 0.0))
|
||||
|
||||
Reference in New Issue
Block a user