Add main-loop enable/disable per-agent toggle

- self_main_loop.py: 'enabled' map in config (default all True);
  do_check skips disabled agents (marks disabled:true in results);
  new enable/disable actions with optional --agent.
- box-ctl.py: main-loop enable|disable [--agent <name>] actions,
  USAGE updated.

Check skips disabled agents so the loop can be toggled per
Chromebox without stopping the systemd timer.

Session: sidechat/chromebox-ops
This commit is contained in:
operator-main
2026-10-04 18:52:54 +00:00
parent e93e9b6122
commit 0822960810
2 changed files with 70 additions and 13 deletions
+28 -9
View File
@@ -650,18 +650,25 @@ def act_notify(agent, message, sidechat=None, allow_main_chat=False):
out(True, agent=agent, target=target, sent=True) 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). """Run the main-chat self-monitor loop (self_main_loop.py).
check: one loop iteration -> JSON {ok, prompted, errors, new_total, agents} check: one loop iteration -> JSON {ok, prompted, errors, new_total, agents}
status: watermark / last run / prompt-sidechat config, no chat reads. 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"): if sub not in ("check", "status", "enable", "disable"):
fail("BAD_NAME", "usage: main-loop check|status") fail("BAD_NAME", "usage: main-loop check|status|enable|disable [--agent <name>]")
audit("main-loop-" + sub) 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" script = BIN / "self_main_loop.py"
cmd = [sys.executable, str(script), sub]
if agent:
cmd += ["--agent", agent]
try: try:
r = subprocess.run([sys.executable, str(script), sub], r = subprocess.run(cmd,
capture_output=True, text=True, timeout=600) capture_output=True, text=True, timeout=600)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
fail("LOOP_TIMEOUT", "self_main_loop.py timed out") fail("LOOP_TIMEOUT", "self_main_loop.py timed out")
@@ -1171,7 +1178,8 @@ fleet:
relay-health relay-health
cdp-latency cdp-latency
identity-audit VM identity audit drift check 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 <name>]
main-chat self-monitor; enable/disable per agent or all
timer actions: timer actions:
timer-list timer-list
@@ -1415,9 +1423,20 @@ def main(argv):
fail("BAD_ARGS", "usage: chrome-errors") fail("BAD_ARGS", "usage: chrome-errors")
act_chrome_errors() act_chrome_errors()
elif action == "main-loop": elif action == "main-loop":
if len(rest) != 1 or rest[0] not in ("check", "status"): if not rest or rest[0] not in ("check", "status", "enable", "disable"):
fail("BAD_NAME", "usage: main-loop check|status") fail("BAD_NAME", "usage: main-loop check|status|enable|disable [--agent <name>]")
act_main_loop(rest[0]) 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 <name>]" % sub)
agent = args[i + 1]
args = args[:i] + args[i + 2:]
if args:
fail("BAD_ARGS", "usage: main-loop %s [--agent <name>]" % sub)
act_main_loop(sub, agent)
elif action == "dm-log": elif action == "dm-log":
limit = 50 limit = 50
if rest: if rest:
+40 -2
View File
@@ -93,11 +93,16 @@ def get_config(st):
agents = [a for a in agents if a in DEFAULT_AGENTS] agents = [a for a in agents if a in DEFAULT_AGENTS]
prompt = dict(DEFAULT_PROMPT_SIDECHAT) prompt = dict(DEFAULT_PROMPT_SIDECHAT)
prompt.update(cfg.get("prompt_sidechat") or {}) 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 { return {
"agents": agents or list(DEFAULT_AGENTS), "agents": agents or list(DEFAULT_AGENTS),
"prompt_sidechat": prompt, "prompt_sidechat": prompt,
"sender": cfg.get("sender") or DEFAULT_SENDER, "sender": cfg.get("sender") or DEFAULT_SENDER,
"read_limit": int(cfg.get("read_limit") or READ_LIMIT), "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} return {"ok": False, "error": "unknown agent: %s" % only_agent}
agents_state = st.get("agents") or {} agents_state = st.get("agents") or {}
enabled = cfg.get("enabled") or {}
results = {} results = {}
prompted = 0 prompted = 0
errors = 0 errors = 0
for agent in agents: 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 {} wm = agents_state.get(agent) or {}
ok, payload = read_main_chat(agent, cfg["read_limit"]) ok, payload = read_main_chat(agent, cfg["read_limit"])
if not ok: if not ok:
@@ -231,19 +240,40 @@ def do_check(only_agent=None):
st["config"] = {"agents": cfg["agents"], st["config"] = {"agents": cfg["agents"],
"prompt_sidechat": cfg["prompt_sidechat"], "prompt_sidechat": cfg["prompt_sidechat"],
"sender": cfg["sender"], "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) save_state(st)
return {"ok": errors == 0, "prompted": prompted, "errors": errors, return {"ok": errors == 0, "prompted": prompted, "errors": errors,
"new_total": sum(r.get("new", 0) for r in results.values()), "new_total": sum(r.get("new", 0) for r in results.values()),
"agents": results} "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(): def do_status():
st = load_state() st = load_state()
cfg = get_config(st) cfg = get_config(st)
agents_state = st.get("agents") or {} agents_state = st.get("agents") or {}
return {"ok": True, return {"ok": True,
"config": cfg, "config": cfg,
"enabled": cfg.get("enabled") or {},
"watermark": agents_state, "watermark": agents_state,
"last_run": st.get("last_run"), "last_run": st.get("last_run"),
"last_result": st.get("last_result")} "last_result": st.get("last_result")}
@@ -261,8 +291,16 @@ def main(argv):
print(json.dumps(do_status())) print(json.dumps(do_status()))
return 0 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": 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 return 2
# Overlap guard: timer may fire while a slow run is still going. # Overlap guard: timer may fire while a slow run is still going.