diff --git a/bin/box-ctl.py b/bin/box-ctl.py index e18ee63..0d9f938 100755 --- a/bin/box-ctl.py +++ b/bin/box-ctl.py @@ -650,18 +650,25 @@ def act_notify(agent, message, sidechat=None, allow_main_chat=False): out(True, agent=agent, target=target, sent=True) -def act_main_loop(sub): +def act_main_loop(sub, agent=None): """Run the main-chat self-monitor loop (self_main_loop.py). - check: one loop iteration -> JSON {ok, prompted, errors, new_total, agents} - status: watermark / last run / prompt-sidechat config, no chat reads. + check: one loop iteration -> JSON {ok, prompted, errors, new_total, agents} + status: watermark / last run / prompt-sidechat config, no chat reads. + enable: enable the loop (per-agent or all); check skips disabled agents. + disable: disable the loop (per-agent or all). """ - if sub not in ("check", "status"): - fail("BAD_NAME", "usage: main-loop check|status") - audit("main-loop-" + sub) + if sub not in ("check", "status", "enable", "disable"): + fail("BAD_NAME", "usage: main-loop check|status|enable|disable [--agent ]") + if agent is not None and agent not in VALID_AGENTS: + fail("BAD_NAME", f"agent must be one of {sorted(VALID_AGENTS)}") + audit("main-loop-" + sub + (f"-{agent}" if agent else "")) script = BIN / "self_main_loop.py" + cmd = [sys.executable, str(script), sub] + if agent: + cmd += ["--agent", agent] try: - r = subprocess.run([sys.executable, str(script), sub], + r = subprocess.run(cmd, capture_output=True, text=True, timeout=600) except subprocess.TimeoutExpired: fail("LOOP_TIMEOUT", "self_main_loop.py timed out") @@ -1171,7 +1178,8 @@ fleet: relay-health cdp-latency identity-audit VM identity audit drift check - main-loop check|status main-chat self-monitor: prompt sidechat on new main msgs + main-loop check|status|enable|disable [--agent ] + main-chat self-monitor; enable/disable per agent or all timer actions: timer-list @@ -1415,9 +1423,20 @@ def main(argv): fail("BAD_ARGS", "usage: chrome-errors") act_chrome_errors() elif action == "main-loop": - if len(rest) != 1 or rest[0] not in ("check", "status"): - fail("BAD_NAME", "usage: main-loop check|status") - act_main_loop(rest[0]) + if not rest or rest[0] not in ("check", "status", "enable", "disable"): + fail("BAD_NAME", "usage: main-loop check|status|enable|disable [--agent ]") + sub = rest[0] + agent = None + args = rest[1:] + if "--agent" in args: + i = args.index("--agent") + if i + 1 >= len(args): + fail("BAD_ARGS", "usage: main-loop %s [--agent ]" % sub) + agent = args[i + 1] + args = args[:i] + args[i + 2:] + if args: + fail("BAD_ARGS", "usage: main-loop %s [--agent ]" % sub) + act_main_loop(sub, agent) elif action == "dm-log": limit = 50 if rest: diff --git a/bin/self_main_loop.py b/bin/self_main_loop.py index da1e623..1d3e829 100755 --- a/bin/self_main_loop.py +++ b/bin/self_main_loop.py @@ -93,11 +93,16 @@ def get_config(st): agents = [a for a in agents if a in DEFAULT_AGENTS] prompt = dict(DEFAULT_PROMPT_SIDECHAT) prompt.update(cfg.get("prompt_sidechat") or {}) + enabled = dict(cfg.get("enabled") or {}) + # Default any unlisted agent to enabled. + for a in agents: + enabled.setdefault(a, True) return { "agents": agents or list(DEFAULT_AGENTS), "prompt_sidechat": prompt, "sender": cfg.get("sender") or DEFAULT_SENDER, "read_limit": int(cfg.get("read_limit") or READ_LIMIT), + "enabled": enabled, } @@ -180,11 +185,15 @@ def do_check(only_agent=None): return {"ok": False, "error": "unknown agent: %s" % only_agent} agents_state = st.get("agents") or {} + enabled = cfg.get("enabled") or {} results = {} prompted = 0 errors = 0 for agent in agents: + if not enabled.get(agent, True): + results[agent] = {"ok": True, "new": 0, "disabled": True} + continue wm = agents_state.get(agent) or {} ok, payload = read_main_chat(agent, cfg["read_limit"]) if not ok: @@ -231,19 +240,40 @@ def do_check(only_agent=None): st["config"] = {"agents": cfg["agents"], "prompt_sidechat": cfg["prompt_sidechat"], "sender": cfg["sender"], - "read_limit": cfg["read_limit"]} + "read_limit": cfg["read_limit"], + "enabled": dict(cfg.get("enabled") or + {a: True for a in cfg["agents"]})} save_state(st) return {"ok": errors == 0, "prompted": prompted, "errors": errors, "new_total": sum(r.get("new", 0) for r in results.values()), "agents": results} +def _set_enabled(agent, value): + """Enable/disable the loop for one agent (or all if agent is None).""" + st = load_state() + cfg = st.get("config") or {} + agents = cfg.get("agents") or list(DEFAULT_AGENTS) + agents = [a for a in agents if a in DEFAULT_AGENTS] or list(DEFAULT_AGENTS) + if agent and agent not in DEFAULT_AGENTS: + return {"ok": False, "error": "unknown agent: %s" % agent} + enabled = dict(cfg.get("enabled") or {}) + targets = [agent] if agent else agents + for a in targets: + enabled[a] = value + cfg["enabled"] = enabled + st["config"] = cfg + save_state(st) + return {"ok": True, "enabled": {a: enabled.get(a, True) for a in targets}} + + def do_status(): st = load_state() cfg = get_config(st) agents_state = st.get("agents") or {} return {"ok": True, "config": cfg, + "enabled": cfg.get("enabled") or {}, "watermark": agents_state, "last_run": st.get("last_run"), "last_result": st.get("last_result")} @@ -261,8 +291,16 @@ def main(argv): print(json.dumps(do_status())) return 0 + if action == "enable": + print(json.dumps(_set_enabled(only_agent, True))) + return 0 + + if action == "disable": + print(json.dumps(_set_enabled(only_agent, False))) + return 0 + if action != "check": - print(json.dumps({"ok": False, "error": "usage: check|status [--agent X]"})) + print(json.dumps({"ok": False, "error": "usage: check|status|enable|disable [--agent X]"})) return 2 # Overlap guard: timer may fire while a slow run is still going.